minecodex 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +149 -0
- package/apps/dashboard/DESIGN.md +45 -0
- package/features/images/DESIGN.md +71 -0
- package/features/images/README.md +2 -0
- package/features/images/codex-feature.json +2 -1
- package/features/images/src/adapters/codex/cloud-import.mjs +52 -0
- package/features/images/src/adapters/codex/image-prompt.mjs +22 -0
- package/features/images/src/adapters/codex/library-api.mjs +55 -0
- package/features/images/src/adapters/codex/library-viewer.mjs +18 -0
- package/features/images/src/adapters/codex/prompt-resolver-source.mjs +8 -0
- package/features/images/src/backend/cloud-library.mjs +10 -0
- package/features/images/src/domain/library-prompt.mjs +7 -0
- package/features/images/src/library-prompt-page.mjs +7 -75
- package/features/images/src/library-prompt-source.mjs +10 -0
- package/features/images/web/app.js +9 -9
- package/features/images/web/styles.css +219 -219
- package/features/model-slider/DESIGN.md +34 -0
- package/features/model-slider/src/adapters/codex/controller.mjs +93 -0
- package/features/model-slider/src/domain/model-rules.mjs +143 -0
- package/features/model-slider/src/frontend/install.mjs +2110 -0
- package/features/model-slider/src/injection-source.mjs +10 -0
- package/features/notes/DESIGN.md +13 -15
- package/features/notes/README.md +7 -1
- package/features/notes/assets/icons/book-open-nav-fill.svg +1 -0
- package/features/notes/codex-feature.json +5 -1
- package/features/notes/src/{default-prompts.mjs → backend/default-prompts.mjs} +1 -1
- package/features/notes/src/{product.mjs → backend/product.mjs} +1 -1
- package/features/notes/src/{quick-prompts-server.mjs → backend/quick-prompts-server.mjs} +2 -2
- package/features/notes/src/{quick-prompts-page.mjs → frontend/entry.mjs} +64 -257
- package/features/notes/src/frontend/icons.json +13 -0
- package/features/notes/src/frontend/styles.css +110 -0
- package/features/notes/src/main.mjs +2 -2
- package/features/notes/vendor/phosphor-LICENSE +21 -0
- package/features/session-status/DESIGN.md +10 -8
- package/features/session-status/README.md +4 -0
- package/features/session-status/codex-feature.json +1 -1
- package/features/session-status/src/adapters/codex/context-usage.mjs +46 -0
- package/features/session-status/src/adapters/codex/session-log.mjs +81 -0
- package/features/session-status/src/adapters/codex/session-reader.mjs +106 -0
- package/features/session-status/src/domain/session-stats.mjs +141 -0
- package/features/session-status/src/injection-source.mjs +6 -0
- package/features/session-status/src/session-status.mjs +42 -102
- package/features/sidebar-focus/DESIGN.md +6 -5
- package/features/sidebar-focus/codex-feature.json +2 -1
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +28 -0
- package/features/sidebar-focus/src/injection-source.mjs +6 -0
- package/features/sidebar-focus/src/sidebar-focus.mjs +19 -66
- package/features/sites/DESIGN.md +60 -0
- package/features/sites/README.md +27 -0
- package/features/sites/codex-feature.json +28 -0
- package/features/sites/src/adapters/codex/catalog.mjs +26 -0
- package/features/sites/src/adapters/codex/renderer.mjs +83 -0
- package/features/sites/src/backend/discovery.mjs +73 -0
- package/features/sites/src/backend/project-sites.mjs +34 -0
- package/features/sites/src/backend/site-launcher.mjs +54 -0
- package/features/sites/src/backend/sites-preview.mjs +53 -0
- package/features/sites/src/backend/sites-service.mjs +162 -0
- package/features/sites/src/domain/site.mjs +40 -0
- package/features/sites/src/frontend/entry.mjs +383 -0
- package/features/sites/src/frontend/icons.json +12 -0
- package/features/sites/src/frontend/styles.css +72 -0
- package/features/sites/src/injection-source.mjs +7 -0
- package/features/sites/vendor/lucide-LICENSE +35 -0
- package/features/unread-inbox/DESIGN.md +6 -2
- package/features/unread-inbox/codex-feature.json +2 -1
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +66 -0
- package/features/unread-inbox/src/injection-source.mjs +6 -0
- package/features/unread-inbox/src/unread-inbox.mjs +34 -89
- package/features/usage-meter/DESIGN.md +9 -9
- package/features/usage-meter/README.md +2 -1
- package/features/usage-meter/codex-feature.json +3 -3
- package/features/usage-meter/src/adapters/codex/renderer.mjs +61 -0
- package/features/usage-meter/src/injection-source.mjs +6 -0
- package/features/usage-meter/src/usage-meter.mjs +27 -66
- package/package.json +17 -4
- package/packages/cli/src/config.mjs +2 -1
- package/packages/codex-adapter/README.md +15 -0
- package/packages/codex-adapter/package.json +1 -0
- package/packages/codex-adapter/src/renderer/environment.mjs +53 -0
- package/packages/runtime-host/README.md +27 -0
- package/packages/runtime-host/src/codex-injection.mjs +32 -2393
- package/packages/runtime-host/src/codex-runtime.mjs +21 -3
- package/packages/runtime-host/src/feature-registry.mjs +28 -3
- package/packages/ui/DESIGN.md +31 -0
- package/packages/ui/package.json +8 -0
- package/packages/ui/src/components/button.mjs +21 -0
- package/packages/ui/src/components/composer-menu.mjs +18 -0
- package/packages/ui/src/components/dialog.mjs +55 -0
- package/packages/ui/src/components/navigation-rail.mjs +77 -0
- package/packages/ui/src/components/page.mjs +8 -0
- package/packages/ui/src/components/tooltip.mjs +96 -0
- package/packages/ui/src/foundation/layers.mjs +14 -0
- package/packages/ui/src/foundation/surfaces.mjs +34 -0
- package/packages/ui/src/install.mjs +22 -0
- package/packages/ui/src/native/navigation-rail.mjs +33 -0
- package/packages/ui/src/native/theme.mjs +35 -0
- package/packages/{runtime-host/src/codex-design-contract.mjs → ui/src/native/tokens.mjs} +67 -24
- package/packages/runtime-host/src/cloud-images.mjs +0 -123
- package/packages/runtime-host/src/feature-tooltips.mjs +0 -65
- package/packages/runtime-host/src/session-status-reader.mjs +0 -340
- /package/features/notes/src/{prompt-schema.mjs → backend/prompt-schema.mjs} +0 -0
|
@@ -7,12 +7,14 @@ const copyByLocale = { "zh-CN": {
|
|
|
7
7
|
firstToken: "平均首字延迟", input: "输入", cached: "缓存输入", output: "输出",
|
|
8
8
|
sum: "合计", capacity: "窗口容量", contextShare: "上下文占用", compactions: "压缩次数",
|
|
9
9
|
instructions: "指令", messages: "消息", toolHistory: "工具交互", other: "未分类",
|
|
10
|
+
speedHelp: "关于 TPS", speedInfo: "GPT 等模型不公开完整思考过程及逐 Token 生成时间。这里的 TPS 是基于可观测事件估算的体感响应速度,仅供参考,不代表模型真实的 Token 生成速度。",
|
|
10
11
|
}, en: {
|
|
11
12
|
turns: "turns", steps: "steps", speed: "TPS", cache: "Cache", cacheDetail: "Cache hit",
|
|
12
13
|
statistics: "Session stats", usage: "Token usage", context: "Context", total: "Total",
|
|
13
14
|
firstToken: "Avg first token", input: "Input", cached: "Cached input", output: "Output",
|
|
14
15
|
sum: "Sum", capacity: "Window size", contextShare: "Context used", compactions: "Compactions",
|
|
15
16
|
instructions: "Instructions", messages: "Messages", toolHistory: "Tool activity", other: "Unclassified",
|
|
17
|
+
speedHelp: "About TPS", speedInfo: "Models such as GPT do not expose their full reasoning process or per-token generation timing. This TPS estimate reflects perceived response speed based on observable events, rather than the model’s actual token generation speed.",
|
|
16
18
|
} };
|
|
17
19
|
function selectedCopy() {
|
|
18
20
|
const language = document.documentElement.lang || navigator.languages?.[0] || navigator.language || "en";
|
|
@@ -21,6 +23,8 @@ function selectedCopy() {
|
|
|
21
23
|
|
|
22
24
|
// 图标来自 Lucide Static 与 Phosphor Icons;许可见 vendor/。
|
|
23
25
|
const icons = {
|
|
26
|
+
// Lucide info,使用官方 SVG 路径。
|
|
27
|
+
info: '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
|
|
24
28
|
statistics: '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>',
|
|
25
29
|
usage: '<svg aria-hidden="true" viewBox="0 0 256 256" fill="currentColor"><path d="M128,24C74.17,24,32,48.6,32,80v96c0,31.4,42.17,56,96,56s96-24.6,96-56V80C224,48.6,181.83,24,128,24Zm80,104c0,9.62-7.88,19.43-21.61,26.92C170.93,163.35,150.19,168,128,168s-42.93-4.65-58.39-13.08C55.88,147.43,48,137.62,48,128V111.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64ZM69.61,53.08C85.07,44.65,105.81,40,128,40s42.93,4.65,58.39,13.08C200.12,60.57,208,70.38,208,80s-7.88,19.43-21.61,26.92C170.93,115.35,150.19,120,128,120s-42.93-4.65-58.39-13.08C55.88,99.43,48,89.62,48,80S55.88,60.57,69.61,53.08ZM186.39,202.92C170.93,211.35,150.19,216,128,216s-42.93-4.65-58.39-13.08C55.88,195.43,48,185.62,48,176V159.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64V176C208,185.62,200.12,195.43,186.39,202.92Z"/></svg>',
|
|
26
30
|
};
|
|
@@ -29,7 +33,7 @@ document.getElementById(STYLE_ID)?.remove();
|
|
|
29
33
|
const style = document.createElement("style");
|
|
30
34
|
style.id = STYLE_ID;
|
|
31
35
|
style.textContent = `
|
|
32
|
-
[${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 4px; margin:0 auto; padding-top:2px; max-width:100%; min-height:24px; min-width:0; font-family:var(--font-sans-default,inherit); font-size:var(--text-xs,12px); line-height:16px; font-variant-numeric:tabular-nums; color:var(--color-text-tertiary); }
|
|
36
|
+
[${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 4px; margin:0 auto; padding-top:2px; max-width:100%; min-height:24px; min-width:0; font-family:var(--mcx-font-sans-default,inherit); font-size:var(--mcx-text-xs,12px); line-height:16px; font-variant-numeric:tabular-nums; color:var(--mcx-color-text-tertiary); }
|
|
33
37
|
[${MARKER}][hidden] { display:none; }
|
|
34
38
|
[data-minecodex-session-reveal] { display:grid; grid-template-rows:0fr; opacity:0; transition:grid-template-rows 200ms ease,opacity 160ms ease; }
|
|
35
39
|
[data-minecodex-session-reveal][data-expanded] { grid-template-rows:1fr; opacity:1; }
|
|
@@ -39,40 +43,44 @@ style.textContent = `
|
|
|
39
43
|
[data-minecodex-session-reveal]:not([data-expanded]) { pointer-events:none; }
|
|
40
44
|
[data-minecodex-session-reveal][hidden] { display:none; }
|
|
41
45
|
/* 仅展开状态行时缩减原生底部留白;收起或卸载后自动恢复。 */
|
|
42
|
-
|
|
43
|
-
[data-minecodex-context-trigger] { display:inline-flex; align-items:center; justify-content:center; flex:none; width:24px; height:var(--height-token-button-composer,28px); padding:0; border:0; border-radius:var(--radius-full); background:transparent; color:var(--color-text-tertiary); cursor:pointer; }
|
|
46
|
+
[data-thread-scroll-footer="true"]:has([data-minecodex-session-reveal][data-expanded]:not([hidden])) { --thread-composer-bottom-inset:4px; }
|
|
47
|
+
[data-minecodex-context-trigger] { display:inline-flex; align-items:center; justify-content:center; flex:none; width:24px; height:var(--height-token-button-composer,28px); padding:0; border:0; border-radius:var(--mcx-radius-full); background:transparent; color:var(--mcx-color-text-tertiary); cursor:pointer; }
|
|
44
48
|
[data-minecodex-context-trigger][hidden] { display:none; }
|
|
45
49
|
[data-context-progress] { transition:stroke-dashoffset 120ms ease-out; }
|
|
46
|
-
[data-minecodex-context-trigger]:hover { background:var(--color-background-primary-ghost-hover); }
|
|
47
|
-
[data-minecodex-context-trigger]:focus-visible { outline:2px solid var(--color-ring); }
|
|
50
|
+
[data-minecodex-context-trigger]:hover { background:var(--mcx-color-background-primary-ghost-hover); }
|
|
51
|
+
[data-minecodex-context-trigger]:focus-visible { outline:2px solid var(--mcx-color-ring); }
|
|
48
52
|
[data-minecodex-context-trigger] svg { width:12px; height:12px; }
|
|
49
53
|
[data-minecodex-native-context] { display:none !important; }
|
|
50
|
-
[data-minecodex-context-tooltip] {
|
|
54
|
+
[data-minecodex-context-tooltip] { width:max-content; text-align:center; white-space:pre-line; }
|
|
51
55
|
@media (prefers-reduced-motion:reduce) { [data-minecodex-session-reveal], [data-context-progress] { transition:none; } }
|
|
52
|
-
[${MARKER}] button { box-sizing:border-box; display:inline-flex; align-items:center; gap:6px; height:22px; color:inherit; font:inherit; border:0; border-radius:var(--radius-xl,12px); background:transparent; padding:3px 6px; cursor:pointer; white-space:nowrap; }
|
|
56
|
+
[${MARKER}] button { box-sizing:border-box; display:inline-flex; align-items:center; gap:6px; height:22px; color:inherit; font:inherit; border:0; border-radius:var(--mcx-radius-xl,12px); background:transparent; padding:3px 6px; cursor:pointer; white-space:nowrap; }
|
|
53
57
|
[${MARKER}] svg, [data-minecodex-session-status-panel] h3 svg { width:14px; height:14px; flex:none; stroke-width:1.75; }
|
|
54
58
|
[${MARKER}] [data-kind="context"] svg, [data-minecodex-session-status-panel] h3 .context-ring { width:12px; height:12px; stroke-width:2; }
|
|
55
|
-
[${MARKER}] button:hover, [${MARKER}] button[aria-expanded="true"] { background:var(--color-token-list-hover-background); }
|
|
59
|
+
[${MARKER}] button:hover, [${MARKER}] button[aria-expanded="true"] { background:var(--mcx-color-token-list-hover-background); }
|
|
56
60
|
[${MARKER}] button:focus-visible { outline:2px solid var(--input-outline-border-color-focus); outline-offset:1px; }
|
|
57
|
-
[data-minecodex-session-status-panel] { --mcx-purple:var(--app-block-accent-purple,light-dark(#8952ee,#7849d1)); --mcx-blue:var(--app-block-accent-blue,light-dark(#3a83f7,#2c67c5)); --mcx-green:var(--app-block-accent-green,light-dark(#53b559,#48a04c)); box-sizing:border-box; position:fixed;
|
|
58
|
-
[data-minecodex-session-status-panel] h3 { display:flex; align-items:center; gap:8px; margin:0 0 12px; padding:0 0 10px; border-bottom:1px solid var(--color-border-subtle); font-size:14px; font-weight:600; }
|
|
61
|
+
[data-minecodex-session-status-panel] { --mcx-purple:var(--app-block-accent-purple,light-dark(#8952ee,#7849d1)); --mcx-blue:var(--app-block-accent-blue,light-dark(#3a83f7,#2c67c5)); --mcx-green:var(--app-block-accent-green,light-dark(#53b559,#48a04c)); box-sizing:border-box; position:fixed; min-width:260px; max-width:min(360px,calc(100vw - 24px)); padding:16px; border-radius:var(--mcx-radius-xl,14px); color:var(--mcx-color-text-primary); font-family:var(--mcx-font-sans-default,inherit); font-size:13px; }
|
|
62
|
+
[data-minecodex-session-status-panel] h3 { display:flex; align-items:center; gap:8px; margin:0 0 12px; padding:0 0 10px; border-bottom:1px solid var(--mcx-color-border-subtle); font-size:14px; font-weight:600; }
|
|
59
63
|
[data-minecodex-session-status-panel] dl { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px 20px; margin:0; }
|
|
60
|
-
[data-minecodex-session-status-panel] dt { color:var(--color-text-tertiary); }
|
|
64
|
+
[data-minecodex-session-status-panel] dt { color:var(--mcx-color-text-tertiary); }
|
|
65
|
+
[data-minecodex-session-status-panel] .tps-label { display:flex; align-items:center; gap:6px; }
|
|
66
|
+
[data-minecodex-session-status-panel] .tps-info { display:inline-flex; align-items:center; padding:0; border:0; background:transparent; color:inherit; cursor:help; }
|
|
67
|
+
[data-minecodex-session-status-panel] .tps-info svg { width:12px; height:12px; }
|
|
68
|
+
[data-minecodex-session-status-panel] .tps-info:focus-visible { outline:2px solid var(--mcx-color-ring); outline-offset:2px; border-radius:var(--mcx-radius-full); }
|
|
61
69
|
[data-minecodex-session-status-panel] dd { margin:0; text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
62
70
|
[data-minecodex-session-status-panel] .context-head { display:flex; justify-content:space-between; gap:12px; margin-bottom:8px; }
|
|
63
|
-
[data-minecodex-session-status-panel] .context-head span:first-child { color:var(--color-text-tertiary); }
|
|
64
|
-
[data-minecodex-session-status-panel] .context-bar, [data-minecodex-session-status-panel] .mix-bar { display:flex; height:6px; overflow:hidden; border-radius:var(--radius-xl); background:var(--color-token-border-default); }
|
|
71
|
+
[data-minecodex-session-status-panel] .context-head span:first-child { color:var(--mcx-color-text-tertiary); }
|
|
72
|
+
[data-minecodex-session-status-panel] .context-bar, [data-minecodex-session-status-panel] .mix-bar { display:flex; height:6px; overflow:hidden; border-radius:var(--mcx-radius-xl); background:var(--color-token-border-default); }
|
|
65
73
|
[data-minecodex-session-status-panel] .context-bar { margin-bottom:14px; }
|
|
66
74
|
[data-minecodex-session-status-panel] .mix-bar { grid-column:1 / -1; margin:0 0 3px; }
|
|
67
75
|
[data-minecodex-session-status-panel] .context-bar span, [data-minecodex-session-status-panel] .mix-bar span { display:block; flex:none; height:100%; }
|
|
68
76
|
[data-minecodex-session-status-panel] .context-bar [data-segment="instructions"] { background:var(--mcx-purple); }
|
|
69
77
|
[data-minecodex-session-status-panel] .context-bar [data-segment="messages"], [data-minecodex-session-status-panel] .context-bar [data-segment="overall"] { background:var(--mcx-blue); }
|
|
70
78
|
[data-minecodex-session-status-panel] .context-bar [data-segment="tools"] { background:var(--mcx-green); }
|
|
71
|
-
[data-minecodex-session-status-panel] .context-bar [data-segment="other"] { background:var(--color-text-tertiary); opacity:.4; }
|
|
79
|
+
[data-minecodex-session-status-panel] .context-bar [data-segment="other"] { background:var(--mcx-color-text-tertiary); opacity:.4; }
|
|
72
80
|
[data-minecodex-session-status-panel] .mix-bar [data-tone="purple"], [data-minecodex-session-status-panel] dt[data-tone="purple"]::before { background:var(--mcx-purple); }
|
|
73
81
|
[data-minecodex-session-status-panel] .mix-bar [data-tone="blue"], [data-minecodex-session-status-panel] dt[data-tone="blue"]::before { background:var(--mcx-blue); }
|
|
74
82
|
[data-minecodex-session-status-panel] .mix-bar [data-tone="green"], [data-minecodex-session-status-panel] dt[data-tone="green"]::before { background:var(--mcx-green); }
|
|
75
|
-
[data-minecodex-session-status-panel] .detail-divider { grid-column:1 / -1; margin-top:4px; border-top:1px solid var(--color-border-subtle); }
|
|
83
|
+
[data-minecodex-session-status-panel] .detail-divider { grid-column:1 / -1; margin-top:4px; border-top:1px solid var(--mcx-color-border-subtle); }
|
|
76
84
|
[data-minecodex-session-status-panel] dt[data-tone] { display:flex; align-items:center; gap:7px; }
|
|
77
85
|
[data-minecodex-session-status-panel] dt[data-tone]::before { content:""; flex:none; width:7px; height:7px; border-radius:2px; }
|
|
78
86
|
`;
|
|
@@ -94,11 +102,7 @@ window.addEventListener("storage", (event) => {
|
|
|
94
102
|
}, { signal });
|
|
95
103
|
// Codex 会把离屏输入框搬到 Quick Chat 浮层;节点仍连接不代表当前页面是聊天。
|
|
96
104
|
function isActiveComposer(composer) {
|
|
97
|
-
|
|
98
|
-
return Boolean(composer.isConnected
|
|
99
|
-
&& card?.getAttribute('data-composer-placement') === 'thread'
|
|
100
|
-
&& !composer.closest('[hidden], [inert], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
|
|
101
|
-
&& composer.checkVisibility({ visibilityProperty: true }));
|
|
105
|
+
return Boolean(config.codex.composer(composer));
|
|
102
106
|
}
|
|
103
107
|
function createInstance(composerElement) {
|
|
104
108
|
let copy = selectedCopy();
|
|
@@ -144,6 +148,7 @@ trigger.setAttribute("aria-controls", root.id);
|
|
|
144
148
|
trigger.append(createContextRing());
|
|
145
149
|
const tooltip = document.createElement("div");
|
|
146
150
|
tooltip.id = `minecodex-context-tooltip-${instanceId}`;
|
|
151
|
+
config.ui.tooltipSurface(tooltip);
|
|
147
152
|
tooltip.dataset.minecodexContextTooltip = "";
|
|
148
153
|
tooltip.setAttribute("role", "tooltip");
|
|
149
154
|
tooltip.hidden = true;
|
|
@@ -217,6 +222,11 @@ collapse.innerHTML = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" st
|
|
|
217
222
|
collapse.addEventListener("click", () => { setGlobalExpanded(false); trigger.focus(); hideTooltip(); }, { signal });
|
|
218
223
|
root.append(collapse);
|
|
219
224
|
let snapshot = null;
|
|
225
|
+
// 统计刷新会重建浮窗,复用图标节点以保留 tooltip 的悬浮锚点。
|
|
226
|
+
const tpsInfo = document.createElement("button");
|
|
227
|
+
tpsInfo.type = "button";
|
|
228
|
+
tpsInfo.className = "tps-info";
|
|
229
|
+
tpsInfo.innerHTML = icons.info;
|
|
220
230
|
let panel = null;
|
|
221
231
|
let openKind = null;
|
|
222
232
|
let rescanQueued = false;
|
|
@@ -308,6 +318,7 @@ function showPanel(kind) {
|
|
|
308
318
|
anchor.setAttribute("aria-expanded", "true");
|
|
309
319
|
panel = document.createElement("div");
|
|
310
320
|
panel.dataset.minecodexSessionStatusPanel = kind;
|
|
321
|
+
config.ui.surface(panel, "popover");
|
|
311
322
|
panel.setAttribute("role", "dialog");
|
|
312
323
|
panel.setAttribute("aria-label", copy[kind]);
|
|
313
324
|
const heading = document.createElement("h3");
|
|
@@ -319,7 +330,11 @@ function showPanel(kind) {
|
|
|
319
330
|
row(dl, copy.turns, number(snapshot.turns));
|
|
320
331
|
row(dl, copy.steps, number(snapshot.steps));
|
|
321
332
|
row(dl, copy.firstToken, duration(snapshot.averageFirstTokenMs));
|
|
322
|
-
row(dl, copy.speed, snapshot.observedTps == null ? "—" : `${number(snapshot.observedTps)} tok/s`);
|
|
333
|
+
const speedLabel = row(dl, copy.speed, snapshot.observedTps == null ? "—" : `${number(snapshot.observedTps)} tok/s`);
|
|
334
|
+
speedLabel.className = "tps-label";
|
|
335
|
+
tpsInfo.setAttribute("aria-label", copy.speedHelp);
|
|
336
|
+
tpsInfo.setAttribute("data-minecodex-tooltip", copy.speedInfo);
|
|
337
|
+
speedLabel.append(tpsInfo);
|
|
323
338
|
} else if (kind === "usage") {
|
|
324
339
|
row(dl, copy.total, tokenValue(snapshot.totalTokens));
|
|
325
340
|
const otherInput = snapshot.inputTokens == null || snapshot.cachedInputTokens == null
|
|
@@ -386,88 +401,13 @@ function findComposer() {
|
|
|
386
401
|
return isActiveComposer(composerElement) ? composerElement : null;
|
|
387
402
|
}
|
|
388
403
|
function currentThreadId(composer) {
|
|
389
|
-
|
|
390
|
-
const card = composer.closest('[data-codex-composer-root]');
|
|
391
|
-
const portal = card?.querySelector('[data-above-composer-portal]');
|
|
392
|
-
// 只认 Codex 输入框绑定的会话;侧栏和 URL 在切换模式时可能仍指向旧会话。
|
|
393
|
-
if (card?.getAttribute('data-composer-placement') !== 'thread') return null;
|
|
394
|
-
const value = portal?.getAttribute('data-above-composer-conversation-id');
|
|
395
|
-
return value?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? null;
|
|
404
|
+
return config.codex.currentThreadId(composer);
|
|
396
405
|
}
|
|
397
406
|
// 与原生 Context 指示器订阅同一会话状态;不以日志百分比覆盖它。
|
|
398
|
-
const emptyNativeContext = () => ({ contextUsedTokens:
|
|
407
|
+
const emptyNativeContext = () => ({ contextUsedTokens:null,contextWindow:null,contextPercent:null });
|
|
399
408
|
let nativeContext = emptyNativeContext();
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
let nativeContextThread = null;
|
|
403
|
-
function findNativeScope(element) {
|
|
404
|
-
let fiber;
|
|
405
|
-
for (let node = element; node && !fiber; node = node.parentElement) {
|
|
406
|
-
fiber = node[Object.keys(node).find((key) => key.startsWith("__reactFiber"))];
|
|
407
|
-
}
|
|
408
|
-
for (let depth = 0; fiber && depth < 200; depth++, fiber = fiber.return) {
|
|
409
|
-
const chain = fiber.memoizedProps?.value;
|
|
410
|
-
if (!(chain instanceof Map)) continue;
|
|
411
|
-
for (const node of chain.values()) {
|
|
412
|
-
if (node?.token?.__scopeBrand === "AppScope" && typeof node.store?.sub === "function") {
|
|
413
|
-
return { node, chain };
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
function contextFromNativeUsage(usage) {
|
|
420
|
-
const windowSize = usage?.modelContextWindow;
|
|
421
|
-
const total = usage?.last?.totalTokens;
|
|
422
|
-
if (!Number.isFinite(windowSize) || windowSize <= 0 || !Number.isFinite(total) || total < 0) {
|
|
423
|
-
return emptyNativeContext();
|
|
424
|
-
}
|
|
425
|
-
const used = Math.min(total, windowSize);
|
|
426
|
-
return { contextUsedTokens: used, contextWindow: windowSize, contextPercent: Math.round(used / windowSize * 100) };
|
|
427
|
-
}
|
|
428
|
-
function syncNativeContext(composer) {
|
|
429
|
-
if (nativeContextThread === activeThreadId && nativeContextSubscription) return;
|
|
430
|
-
nativeContextSubscription?.();
|
|
431
|
-
nativeContextSubscription = null;
|
|
432
|
-
nativeContextThread = null;
|
|
433
|
-
nativeContext = emptyNativeContext();
|
|
434
|
-
if (!activeThreadId || !nativeContextFamily) return;
|
|
435
|
-
const scope = findNativeScope(composer);
|
|
436
|
-
if (!scope) return;
|
|
437
|
-
try {
|
|
438
|
-
const threadId = activeThreadId;
|
|
439
|
-
const descriptor = nativeContextFamily.resolve(scope.node, scope.chain, threadId);
|
|
440
|
-
const atom = descriptor.resolve(scope.node, scope.chain);
|
|
441
|
-
const update = () => {
|
|
442
|
-
if (disposed || activeThreadId !== threadId) return;
|
|
443
|
-
nativeContext = contextFromNativeUsage(scope.node.store.get(atom));
|
|
444
|
-
render();
|
|
445
|
-
};
|
|
446
|
-
nativeContextThread = threadId;
|
|
447
|
-
nativeContextSubscription = scope.node.store.sub(atom, update);
|
|
448
|
-
update();
|
|
449
|
-
} catch {
|
|
450
|
-
nativeContextThread = null;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
async function loadNativeContextFamily() {
|
|
454
|
-
try {
|
|
455
|
-
const url = [...document.querySelectorAll("link[rel=modulepreload]")].map((link) => link.href)
|
|
456
|
-
.find((name) => /\/app-shared-[^/]+\.js(?:$|\?)/.test(name));
|
|
457
|
-
if (!url) return;
|
|
458
|
-
const source = await (await fetch(url, { signal })).text();
|
|
459
|
-
// 从当前构建解析导出,避免依赖每次更新都会变化的压缩符号或资源哈希。
|
|
460
|
-
const local = source.match(/([\w$]+)=[\w$]+\(\(\{latestTokenUsageInfo:/)?.[1];
|
|
461
|
-
if (!local) return;
|
|
462
|
-
const exported = [...source.matchAll(/([\w$]+) as ([\w$]+)/g)]
|
|
463
|
-
.find((match) => match[1] === local)?.[2];
|
|
464
|
-
if (!exported) return;
|
|
465
|
-
const module = await import(url);
|
|
466
|
-
if (signal.aborted || disposed) return;
|
|
467
|
-
nativeContextFamily = module[exported];
|
|
468
|
-
syncNativeContext(findComposer());
|
|
469
|
-
} catch { /* 原生接口不可用时保留占位,不显示过期的日志百分比。 */ }
|
|
470
|
-
}
|
|
409
|
+
const nativeUsage = createContextUsageAdapter(signal, config.codex, value => { nativeContext=value; render(); });
|
|
410
|
+
function syncNativeContext(composer) { nativeUsage.connect(activeThreadId, composer); }
|
|
471
411
|
function requestRefresh() {
|
|
472
412
|
if (!activeThreadId || signal.aborted) return;
|
|
473
413
|
if (typeof window[config.bindingName] === "function") {
|
|
@@ -586,7 +526,7 @@ const localeObserver = new MutationObserver(() => {
|
|
|
586
526
|
localeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
|
|
587
527
|
setExpanded(globalExpanded);
|
|
588
528
|
ensurePosition();
|
|
589
|
-
|
|
529
|
+
|
|
590
530
|
const receiver = {
|
|
591
531
|
update(value, revision) {
|
|
592
532
|
ensurePosition();
|
|
@@ -604,7 +544,7 @@ const receiver = {
|
|
|
604
544
|
return { update: receiver.update, setExpanded, dispose() {
|
|
605
545
|
controller.abort();
|
|
606
546
|
disposed = true;
|
|
607
|
-
|
|
547
|
+
nativeUsage.dispose();
|
|
608
548
|
clearTimeout(refreshTimer);
|
|
609
549
|
clearTimeout(collapseTimer);
|
|
610
550
|
observer.disconnect();
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# Sidebar Focus
|
|
2
2
|
|
|
3
|
+
通用视觉、材质、按钮、Tooltip 与原生 sidebar 层级遵循根 [DESIGN.md](../../DESIGN.md)。本文件只记录功能特有组件、布局、数据语义与通用组件选型。
|
|
4
|
+
|
|
3
5
|
保留当前任务所在分类与 Project,收起其他分支。再次点击恢复进入聚焦前的展开状态;聚焦期间切换任务跟随路径。用户手动调整分支时退出聚焦并保留手动布局,无撤销浮层。
|
|
4
6
|
|
|
5
7
|
- 入口:展开侧栏的 Activity 与 Search 之间,随原生侧栏一起隐藏。
|
|
6
|
-
-
|
|
7
|
-
- 控件:继承原生 Activity 的 Button
|
|
8
|
-
- 字体、颜色、圆角、动效全部使用原生控件,不新增主题覆盖。
|
|
8
|
+
- 图标:固定使用 Lucide list-chevrons-down-up,开启和退出聚焦不切换图标。16px,24px viewBox,描边 1.575 units(实际 1.05px)。
|
|
9
|
+
- 控件:继承原生 Activity 的 Button 类与 28px 热区。未激活为 secondary / ghost,激活为 info / soft,与原生 View activity 一致;aria-pressed 表达聚焦开关状态。
|
|
9
10
|
- 中英文标题:Focus current task / 聚焦当前任务;Exit focus / 退出聚焦。
|
|
10
11
|
- 折叠通过原生回调执行;React 元数据仅用于读取稳定分类、项目和会话身份。
|
|
11
12
|
- 无可识别当前路径时禁用;Activity 列表展示期间暂停整理。
|
|
12
13
|
- 状态仅作用于当前窗口;卸载不额外改变用户布局。
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
根据聚焦状态显式设置 data-color 与 data-variant,直接复用原生语义 token 适配明暗模式,不写死蓝色或背景色。Activity 开启时按钮原生 disabled,暂停聚焦操作,关闭后恢复可用性。
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Tooltip 由共享组件库通过 `data-minecodex-tooltip` 管理;插件只提供随聚焦状态变化的文案,不维护独立材质、定位或显示事件。克隆入口时移除原控件的 `title` 与 `aria-describedby`。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function createFocusAdapter(codex) {
|
|
2
|
+
const sectionSelector="[data-app-action-sidebar-section]", projectSelector="[data-app-action-sidebar-project-row]";
|
|
3
|
+
const props = (element,predicate) => codex.props(element,predicate,30) ?? {};
|
|
4
|
+
function branches() {
|
|
5
|
+
return [...document.querySelectorAll("#app-shell-sidebar " + sectionSelector)].map(element => {
|
|
6
|
+
const native = props(element, p => !!p.sectionKey);
|
|
7
|
+
const data = props(element, p => Array.isArray(p.section?.threadKeys)).section;
|
|
8
|
+
return { element, key:"section:" + native.sectionKey, data, expanded:element.dataset.appActionSidebarSectionCollapsed === "false",
|
|
9
|
+
toggle:() => native.onCollapsedChange?.(element.dataset.appActionSidebarSectionCollapsed === "false") };
|
|
10
|
+
}).concat([...document.querySelectorAll("#app-shell-sidebar " + projectSelector)].map(element => ({
|
|
11
|
+
element, key:"project:" + element.dataset.appActionSidebarProjectId,
|
|
12
|
+
data:props(element, p => !!p.group?.projectId).group,
|
|
13
|
+
expanded:element.getAttribute("aria-expanded") === "true",
|
|
14
|
+
toggle:() => element.click(),
|
|
15
|
+
})));
|
|
16
|
+
}
|
|
17
|
+
function target(items) {
|
|
18
|
+
const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
|
|
19
|
+
const id = codex.currentThreadId();
|
|
20
|
+
const key = selected?.dataset.appActionSidebarThreadId || (id ? "local:" + id : null);
|
|
21
|
+
const activeProject = items.map(b => props(b.element, p => p.activeProjectId != null).activeProjectId).find(Boolean);
|
|
22
|
+
const project = items.find(b => b.key.startsWith("project:") && (b.data?.threadKeys?.includes(key) || b.data?.projectId === activeProject));
|
|
23
|
+
const section = items.find(b => b.key.startsWith("section:") && (b.data?.threadKeys?.includes(key) || (activeProject && b.data?.itemKeys?.includes("codex:project:" + activeProject))))
|
|
24
|
+
|| items.find(b => b.key.startsWith("section:") && (selected && b.element.contains(selected) || project && b.element.contains(project.element)));
|
|
25
|
+
return section ? { section, project } : null;
|
|
26
|
+
}
|
|
27
|
+
return {branches,target};
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createFocusAdapter } from "./adapters/codex/renderer.mjs";
|
|
3
|
+
export async function createPageScriptSource() {
|
|
4
|
+
const source=await readFile(new URL("./sidebar-focus.mjs",import.meta.url),"utf8");
|
|
5
|
+
return `const createFocusAdapter=${createFocusAdapter.toString()};\n${source}`;
|
|
6
|
+
}
|
|
@@ -1,81 +1,34 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const nativeFocus = createFocusAdapter(config.codex);
|
|
2
|
+
const FOCUS_GLYPH = "<svg aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.575\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M3 5h8\" />\n <path d=\"M3 12h8\" />\n <path d=\"M3 19h8\" />\n <path d=\"m15 5 3 3 3-3\" />\n <path d=\"m15 19 3-3 3 3\" />\n</svg>\n";
|
|
3
|
+
|
|
4
|
+
|
|
5
5
|
const states = new Map();
|
|
6
|
-
const tip = document.createElement("div");
|
|
7
|
-
tip.id = "minecodex-focus-tooltip";
|
|
8
|
-
tip.setAttribute("role", "tooltip");
|
|
9
|
-
tip.hidden = true;
|
|
10
|
-
const style = document.createElement("style");
|
|
11
|
-
style.textContent = `#minecodex-focus-tooltip {position:fixed;z-index:2147483000;pointer-events:none;padding:5px 12px;border:1px solid var(--color-border-tooltip);border-radius:var(--radius-2xl);background:var(--color-background-tooltip);color:var(--color-text-tooltip);box-shadow:var(--shadow-tooltip);font-family:var(--font-sans-default);font-size:var(--text-sm);line-height:18px;font-weight:var(--tooltip-compact-font-weight);letter-spacing:var(--tracking-tooltip);}`;
|
|
12
|
-
document.head.append(style);
|
|
13
|
-
document.body.append(tip);
|
|
14
|
-
function showTip() {
|
|
15
|
-
if (!button) return;
|
|
16
|
-
tip.hidden = false;
|
|
17
|
-
const rect = button.getBoundingClientRect();
|
|
18
|
-
tip.style.left = Math.max(8, rect.x + rect.width / 2 - tip.offsetWidth / 2) + "px";
|
|
19
|
-
tip.style.top = rect.bottom + 8 + "px";
|
|
20
|
-
}
|
|
21
|
-
function hideTip() { tip.hidden = true; }
|
|
22
6
|
let active = false, restoring = false, button, timer, disposed = false;
|
|
23
7
|
const text = (en, cn) => (document.documentElement.lang || navigator.language).startsWith("zh") ? cn : en;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (let i = 0; fiber && i < 30; i++, fiber = fiber.return) {
|
|
27
|
-
if (predicate(fiber.memoizedProps || {})) return fiber.memoizedProps;
|
|
28
|
-
}
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
function branches() {
|
|
32
|
-
return [...document.querySelectorAll("#app-shell-sidebar " + sectionSelector)].map(element => {
|
|
33
|
-
const native = props(element, p => !!p.sectionKey);
|
|
34
|
-
const data = props(element, p => Array.isArray(p.section?.threadKeys)).section;
|
|
35
|
-
return { element, key:"section:" + native.sectionKey, data, expanded:element.dataset.appActionSidebarSectionCollapsed === "false",
|
|
36
|
-
toggle:() => native.onCollapsedChange?.(element.dataset.appActionSidebarSectionCollapsed === "false") };
|
|
37
|
-
}).concat([...document.querySelectorAll("#app-shell-sidebar " + projectSelector)].map(element => ({
|
|
38
|
-
element, key:"project:" + element.dataset.appActionSidebarProjectId,
|
|
39
|
-
data:props(element, p => !!p.group?.projectId).group,
|
|
40
|
-
expanded:element.getAttribute("aria-expanded") === "true",
|
|
41
|
-
toggle:() => element.click(),
|
|
42
|
-
})));
|
|
43
|
-
}
|
|
8
|
+
|
|
9
|
+
|
|
44
10
|
function remember(items) {
|
|
45
11
|
for (const item of items) if (!states.has(item.key)) states.set(item.key, item.expanded);
|
|
46
12
|
}
|
|
47
|
-
|
|
48
|
-
const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
|
|
49
|
-
const id = document.querySelector("[data-above-composer-conversation-id]")?.dataset.aboveComposerConversationId;
|
|
50
|
-
const key = selected?.dataset.appActionSidebarThreadId || (id ? "local:" + id : null);
|
|
51
|
-
const activeProject = items.map(b => props(b.element, p => p.activeProjectId != null).activeProjectId).find(Boolean);
|
|
52
|
-
const project = items.find(b => b.key.startsWith("project:") && (b.data?.threadKeys?.includes(key) || b.data?.projectId === activeProject));
|
|
53
|
-
const section = items.find(b => b.key.startsWith("section:") && (b.data?.threadKeys?.includes(key) || (activeProject && b.data?.itemKeys?.includes("codex:project:" + activeProject))))
|
|
54
|
-
|| items.find(b => b.key.startsWith("section:") && (selected && b.element.contains(selected) || project && b.element.contains(project.element)));
|
|
55
|
-
return section ? { section, project } : null;
|
|
56
|
-
}
|
|
13
|
+
|
|
57
14
|
function reconcile() {
|
|
58
15
|
if (disposed) return;
|
|
59
16
|
const sidebar = document.querySelector("#app-shell-sidebar");
|
|
60
17
|
const bell = [...(sidebar?.querySelectorAll("button[aria-pressed]") || [])].find(e => /activity|活动|动态/.test(e.getAttribute("aria-label") || ""));
|
|
61
|
-
if (!bell) { button?.remove(); button = null;
|
|
18
|
+
if (!bell) { button?.remove(); button = null; return; }
|
|
62
19
|
if (!button?.isConnected) {
|
|
63
20
|
button = bell.cloneNode(false);
|
|
64
21
|
button.removeAttribute("id");
|
|
65
22
|
button.setAttribute("data-minecodex-focus", "");
|
|
66
23
|
// 复用原生内层容器,由其负责图标在热区内居中。
|
|
67
24
|
const inner = bell.firstElementChild.cloneNode(false);
|
|
68
|
-
inner.innerHTML =
|
|
25
|
+
inner.innerHTML = FOCUS_GLYPH;
|
|
69
26
|
button.replaceChildren(inner);
|
|
70
|
-
button.
|
|
71
|
-
button.
|
|
72
|
-
button.addEventListener("pointerleave", hideTip, { signal });
|
|
73
|
-
button.addEventListener("focus", showTip, { signal });
|
|
74
|
-
button.addEventListener("blur", hideTip, { signal });
|
|
27
|
+
button.removeAttribute("title");
|
|
28
|
+
button.removeAttribute("aria-describedby");
|
|
75
29
|
button.addEventListener("click", () => {
|
|
76
|
-
hideTip();
|
|
77
30
|
if (active) { active = false; restoring = true; }
|
|
78
|
-
else { states.clear(); remember(branches()); active = true; }
|
|
31
|
+
else { states.clear(); remember(nativeFocus.branches()); active = true; }
|
|
79
32
|
schedule();
|
|
80
33
|
}, { signal });
|
|
81
34
|
// 作为原生按钮组的独立子项,继承组内 gap,不放进铃铛的 Tooltip 包装层。
|
|
@@ -87,18 +40,18 @@ function reconcile() {
|
|
|
87
40
|
}
|
|
88
41
|
const label = active ? text("Exit focus", "退出聚焦") : text("Focus current task", "聚焦当前任务");
|
|
89
42
|
if (button.getAttribute("aria-label") !== label) button.setAttribute("aria-label", label);
|
|
90
|
-
|
|
43
|
+
button.setAttribute("data-minecodex-tooltip", label);
|
|
91
44
|
const pressed = String(active);
|
|
92
45
|
if (button.getAttribute("aria-pressed") !== pressed) {
|
|
93
46
|
button.setAttribute("aria-pressed", pressed);
|
|
94
|
-
button.firstElementChild.innerHTML = active ? EXPAND_GLYPH : COLLAPSE_GLYPH;
|
|
95
47
|
}
|
|
96
|
-
|
|
97
|
-
button.setAttribute("data-
|
|
98
|
-
|
|
48
|
+
// 与原生 View activity 共用 info / soft 激活态及明暗主题 token。
|
|
49
|
+
button.setAttribute("data-color", active ? "info" : "secondary");
|
|
50
|
+
button.setAttribute("data-variant", active ? "soft" : "ghost");
|
|
51
|
+
const items = nativeFocus.branches(), path = nativeFocus.target(items);
|
|
99
52
|
const activityActive = bell.getAttribute("aria-pressed") === "true";
|
|
100
53
|
button.disabled = activityActive || (!active && !restoring && !path);
|
|
101
|
-
if (activityActive)
|
|
54
|
+
if (activityActive) return;
|
|
102
55
|
if (restoring) {
|
|
103
56
|
// 先恢复已挂载的子分支,再恢复分类,避免父节点收起后子节点消失。
|
|
104
57
|
const change = items.filter(b => b.key.startsWith("project:")).concat(items.filter(b => b.key.startsWith("section:")))
|
|
@@ -131,4 +84,4 @@ observer.observe(document.documentElement, { subtree:true, childList:true, attri
|
|
|
131
84
|
"data-app-action-sidebar-section-collapsed", "data-above-composer-conversation-id", "lang"
|
|
132
85
|
] });
|
|
133
86
|
reconcile();
|
|
134
|
-
return () => { disposed = true; clearTimeout(timer); observer.disconnect(); button?.remove();
|
|
87
|
+
return () => { disposed = true; clearTimeout(timer); observer.disconnect(); button?.remove(); };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Sites 扩展设计约定
|
|
2
|
+
|
|
3
|
+
通用视觉、材质、按钮、Tooltip 与原生 sidebar 层级遵循根 [DESIGN.md](../../DESIGN.md)。本文件只记录功能特有组件、布局、数据语义与通用组件选型。
|
|
4
|
+
|
|
5
|
+
状态:原生 Sites 增强,沿用宿主当前主题。开发来源与托管位置分开。
|
|
6
|
+
|
|
7
|
+
## 用户路径
|
|
8
|
+
|
|
9
|
+
Sites 标题工具区增加次要操作 Add site,保留官方 Create。添加弹窗默认展示 Codex 本地项目中的网页入口候选(含未启动网站),不显示重复的项目分组标题,项目名称仅在候选次级文本呈现;候选使用 checkbox 多选,点击整行切换勾选;已收录项勾选且禁用。两个模式底部统一提供 Cancel / Add,项目模式未选择时禁用 Add,点击后才批量收录;取消不保存选择。候选不自动收录;扫描不启动服务。打开已收录的未启动站点时,使用支持的启动方式按需启动。弹窗使用「Project / Custom」分段按钮切换;切换保留已填写的网址;项目模式自动扫描并直接展示候选,不显示搜索框、说明行或刷新按钮。「自定义网址」只提供网址和名称,默认作为外部网址收录,在无项目聊天中打开;不显示开发来源及说明。网址与名称均必填;名称不能仅含空白且最多160字符,网址须为不含嵌入凭证的有效 HTTP(S) 地址。不符合时禁用 Add,输入变化后实时更新。项目关联自动来自扫描,不在编辑弹窗暴露。
|
|
10
|
+
|
|
11
|
+
通过 MineCodex 添加的条目统一展示在 Added by you 标签的列表/网格,Owned by you 和 Shared with you 保持官方条目;使用同一搜索框。标题下只显示主机地址与端口;访问列用图标 + Local address / External site,沿用原生 16px 图标、主文字色、text-sm 与 gap-1.5。Added by you 同时包含 Local 和 External,访问列继续区分地址类型。操作列复用原生外部箭头,在默认浏览器打开;Project 条目菜单只提供移除,Custom URL 可编辑网址与名称,沿用添加校验。移除只移除入口。
|
|
12
|
+
|
|
13
|
+
## 原生 token 与组件
|
|
14
|
+
|
|
15
|
+
- 列表:复用原生 row 的 grid/subgrid、82px 最小高度、16px 内边距、rounded-xl 与 primary-ghost-hover。
|
|
16
|
+
- 网格:原生 210px 卡片,148px 预览与62px信息区;rounded-2xl、border-subtle、surface-card、shadow-sm。
|
|
17
|
+
- 按钮:复用原生 `_Button` 与 `_ButtonInner` 类以及 data-color / data-variant / data-size 状态接口。Add site 为 secondary outline;确认沿用 primary solid。
|
|
18
|
+
- 弹窗:选择通用 Dialog 的 opaque 分支(Sites Share),最大宽560px,不加独立操作区分隔线;当前通过 `config.ui` 接入。通用容器、遮罩、标题、Close及焦点契约遵循根规范。
|
|
19
|
+
- 分段按钮:使用原生 segmented-control 的统一底板、等宽选项及选中块,复用 gap/gutter/background/font-weight/thumb-background/thumb-shadow/option-highlight token;控件尺寸为 control-size-md,支持方向键切换与选中状态语义。
|
|
20
|
+
- 图标:从原生 Sites、Create、行菜单节点克隆 SVG;不重画。扫描结果和添加后的条目异步获取真实网页截图,本地缓存;完成后替换占位。没有截图时保留原生 Sites 图标,不阻塞选择和添加。
|
|
21
|
+
|
|
22
|
+
## 打开与布局
|
|
23
|
+
|
|
24
|
+
通过原生 router 的项目 state、本地执行模式(prefillLocalExecution / prefillComposerMode)新建项目草稿,或定位无项目 thread,随后打开原生浏览器。保留原生已有未发送草稿,不发送消息。源关联失效时提示从可用项目重新添加,不静默变成外部来源。
|
|
25
|
+
|
|
26
|
+
浏览器就绪后收起聊天侧栏,调用原生 Enter full view,使当前网页占据完整工作区,并保留原生 Chat/网页标签及底部聊天输入框。已在 full view 时不反向切换;官方站点沿用原生打开路径,同样进入 full view。
|
|
27
|
+
|
|
28
|
+
## 验证
|
|
29
|
+
|
|
30
|
+
隔离 profile 验证发现、添加、路由、列表/网格、搜索、明暗主题、移除、服务停止后的保留;日常安装不注入开发实现。原生内部选择器变化时显示明确错误,不写死聊天宽度。
|
|
31
|
+
|
|
32
|
+
自定义网址的添加与编辑使用相同的两项表单,不提供来源选择浮层。关闭调用共享外壳的原生 Close;项目网页直接展示扫描结果,不重复显示项目分组标题。添加网址仅在分段面板中填写,不再提供底部重复入口或二级返回跳转。
|
|
33
|
+
|
|
34
|
+
More 菜单复用原生 Sites menu/menuitem/leadingIcon 类与 token:220px 最小宽度、4px gutter、菜单项 5px × 8px 内边距、16px 图标、6px 内容间距;文案为 Edit / Remove。Edit 复用原生 SVG,Remove 使用 Lucide trash-2,随主题继承文字色,悬停/聚焦时图标由75%到100%不透明度。
|
|
35
|
+
|
|
36
|
+
预览图加载使用主题文字色与 surface 混合的6%/16%渐变、1.6s单向扫光与 reduced-motion 规则;图片解码成功后替换骨架,失败才显示图标占位。Project websites 缩略图固定96×60(8:5),各加载状态不改变布局。外部箭头通过 electronBridge.sendMessageFromView 发送 open-in-browser,不使用普通 window.postMessage。
|
|
37
|
+
|
|
38
|
+
图标占位沿用预览区域的横幅比例和尺寸,不退回方形。使用 --color-background-primary-soft 浅灰底与 --color-text-primary 图标色(浅色模式黑色,深色模式对应主题色)。
|
|
39
|
+
|
|
40
|
+
预览区域描边复用原生 Sites 的 inset 0 0 0 1px var(--color-border),覆盖于图片/骨架/占位之上,不增加布局尺寸。项目站点副文本显示项目名;确认运行时附加全角括号中的端口号,例如 VonDoc(49706),未运行仅显示 VonDoc。不在界面展示历史时间。无项目归属的 Custom URL 继续显示地址。
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### 添加弹窗按钮(共享表单外壳)
|
|
44
|
+
|
|
45
|
+
Add / Save 选择通用 primary(solid),Cancel 选择 ghost;有实质功能的次级操作选择 secondary(outline)。项目模式未选择候选、自定义模式表单无效时禁用确认。页面工具区按钮继续复用原生 `_Button`。
|
|
46
|
+
|
|
47
|
+
模式选择使用真正的 segmented button:两项等宽,统一底板,选中块使用原生 thumb 背景及阴影;不再使用 Sites ownership tabs 的独立胶囊外观。尺寸与颜色引用原生 control / segmented-control token,随明暗主题变化。
|
|
48
|
+
|
|
49
|
+
扫描等待使用三行结果骨架:96×60横幅、标题及项目名短条,复用预览图的原生 shimmer token,逐行错开动画相位。仅辅助技术读出查找状态,结果区标记 aria-busy;无虚构进度,扫描完成直接替换,沿用 reduced-motion 静态降级。
|
|
50
|
+
|
|
51
|
+
未选中分段选项使用 secondary-solid 配色:底板 background-secondary-solid,文字 text-secondary-solid,悬浮和按下分别使用 secondary-solid-hover / active;不再单独混合透明色。选中块保持原生 thumb 样式。
|
|
52
|
+
|
|
53
|
+
预览加载骨架绝对铺满图片区域,以窄斜向扫光带呈现明确运动,避免宽缓渐变看起来像静态灰块;图片解码完成才替换,失败才显示图标占位。
|
|
54
|
+
|
|
55
|
+
Custom 地址分类:localhost/回环、IPv4私有网段及链路本地、IPv6私有及链路本地、.local 使用 Local address;公网IP及普通域名使用 External site,不推断访问权限。普通域名不做DNS推断,故内网自定义域名可能归为外部。已有条目读取时同步重新分类。
|
|
56
|
+
|
|
57
|
+
打开 Project 网页前等待新草稿的 ComposerScope focusComposerNonce 与本次导航一致,避免仅检查输入框存在而误用旧聊天。浏览器打开消息同时携带站点目录(无目录时取项目根目录)作为 cwd。
|
|
58
|
+
|
|
59
|
+
新版 Sites 位于 `/space/sites`;注入与离开页面的布局判断使用该路由,移除旧 `/sites` 判断。
|
|
60
|
+
标题区通过 New 的语义定位创建入口;按钮容器复用原生 lg secondary Button,不再假设 New 提供 data-variant。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Local & Self-hosted Sites
|
|
2
|
+
|
|
3
|
+
Extends the native Sites page. **Add site** discovers website entries from local Codex project files, including stopped sites, and supplements them with running HTML services. Select candidates with checkboxes and confirm with Add, or use Custom URL to enter an HTTP/HTTPS URL and name. Both modes provide Cancel / Add; Add stays disabled until the selection or required fields are valid.
|
|
4
|
+
|
|
5
|
+
- Project: opens a native new draft in that project.
|
|
6
|
+
- Projectless chat: reopens the original conversation.
|
|
7
|
+
- External: opens a native projectless draft.
|
|
8
|
+
|
|
9
|
+
Existing unsent drafts are preserved; opening a site never sends a message. Saved entries remain when the service stops. Missing projects/chats prompt removal and addition from an available project. Project entries have no Edit site action; Custom URL editing only exposes URL and name. Remove from Sites only deletes the local shortcut.
|
|
10
|
+
|
|
11
|
+
Opening a saved or official site collapses the chat sidebar and uses the native splitter's maximum browser width once. It does not save a new resize preference or continuously override manual dragging.
|
|
12
|
+
|
|
13
|
+
## Storage and discovery
|
|
14
|
+
|
|
15
|
+
Project entry identity uses project + directory rather than port. `project-history.json` stores actual MineCodex launch times (`lastStartedAt`) and observed online times (`lastSeenAt`). The UI displays the project name and adds the port in parentheses only when confirmed running. Custom URLs without a project display their address. Historical timestamps are not shown or inferred from file timestamps.
|
|
16
|
+
|
|
17
|
+
Local records: `sites/sites.json` under the MineCodex application support directory. `MINECODEX_SITES_DATA_DIR` can isolate the store for development. Native OpenAI site records and sharing permissions are not modified.
|
|
18
|
+
|
|
19
|
+
macOS discovery associates listening processes' working directories with Codex local project roots, then checks HTML responses with bounded time and size. APIs, unrelated processes and redirects are excluded. Simulator preview bridges (`serve-sim` and `swiftui-preview-browser`) are excluded by full process command before any HTTP request: their homepages may execute `simctl` and trigger Xcode installation. Processes whose full command is unavailable are also skipped; manual addition remains available. Windows uses listening-process command lines when they contain a known project path; Windows does not expose the same cwd metadata, so services with relative-only command lines require manual addition. Project file discovery is deterministic code: it recognizes simple Vite / Next dev / Astro dev scripts and package-free directories containing index.html. It traverses up to four levels with a 4,000-directory budget, skipping hidden directories, dependencies and common build outputs. It does not execute config files or use an LLM. External worktrees and remote hosts are not yet included; complex scripts and arbitrary subpath routes remain outside automatic detection. Manual URLs can include paths or self-hosted domains.
|
|
20
|
+
|
|
21
|
+
Discovery never starts services. Opening a saved stopped site can start a supported, already-installed framework or a loopback-only static server; dependencies are not installed and arbitrary npm scripts are not executed. MineCodex stops only its own launched services when its runtime stops. No external network ranges are scanned, and no pages or thumbnails are uploaded. Discovery candidates and saved custom sites request background screenshots through an isolated headless Chrome/Edge profile. Previews are cached locally by URL in `previews/`, outside the site records, and reused after restart. Capture does not block Add/Remove. Browser absence, unavailable pages, simulator bridges, or capture failure leave the native placeholder; private browser login sessions are not reused. Screenshots currently capture a 960×600 viewport after a bounded render wait, and are not live previews.
|
|
22
|
+
|
|
23
|
+
## Native UI boundary
|
|
24
|
+
|
|
25
|
+
The renderer owns `data-mcx-sites` elements; the Local address filter temporarily hides native rows and updates tab selection styling, restoring them on exit. It uses native Sites layout classes, current semantic theme tokens, and the existing Prompts dialog/menu patterns. The route-state and resize adapters depend on native renderer internals and must be verified against app updates. Icon sources: native Sites controls, existing Prompts resources, and the Lucide monitor icon; Lucide license is included in `vendor/`.
|
|
26
|
+
|
|
27
|
+
弹窗与浮层统一调用 UI 组件库提供的 `config.ui`(由 Runtime Host 注入),共享主题材质、标题、Close 和表单操作区;插件只维护内容及交互。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "sites",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"label": {
|
|
6
|
+
"en": "Local & Self-hosted Sites",
|
|
7
|
+
"zh-CN": "本地与自托管站点"
|
|
8
|
+
},
|
|
9
|
+
"description": {
|
|
10
|
+
"en": "Discover project web services and open saved sites in their development context.",
|
|
11
|
+
"zh-CN": "发现项目网页服务,快捷添加,并在对应开发聊天中打开站点。"
|
|
12
|
+
},
|
|
13
|
+
"entry": {
|
|
14
|
+
"kind": "page-script",
|
|
15
|
+
"script": "src/frontend/entry.mjs",
|
|
16
|
+
"sourceFactory": "src/injection-source.mjs",
|
|
17
|
+
"resources": {
|
|
18
|
+
"icons": "src/frontend/icons.json",
|
|
19
|
+
"styles": "src/frontend/styles.css"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"hostActions": [
|
|
23
|
+
"sites"
|
|
24
|
+
],
|
|
25
|
+
"placement": {
|
|
26
|
+
"order": 90
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
// Codex 的项目目录与数据库结构在此转换为插件目录快照。
|
|
5
|
+
export class CodexCatalog {
|
|
6
|
+
constructor(codexHome) { this.codexHome=codexHome; this.status={available:false,reason:"not-read"}; }
|
|
7
|
+
diagnostics() {return {...this.status};}
|
|
8
|
+
async snapshot() {
|
|
9
|
+
let state;
|
|
10
|
+
try { state = JSON.parse(await readFile(path.join(this.codexHome, ".codex-global-state.json"), "utf8")); }
|
|
11
|
+
catch (error) { if (error.code === "ENOENT") {this.status={available:false,reason:"catalog-state-unavailable"};return { projects: [], threads: [] };} throw error; }
|
|
12
|
+
this.status={available:true,reason:null};
|
|
13
|
+
const projects = Object.entries(state["local-projects"] ?? {}).map(([id, p]) => ({ id, name: p.name || id, rootPaths: (p.rootPaths ?? []).filter(r => typeof r === "string") }));
|
|
14
|
+
const ids = new Set(state["projectless-thread-ids"] ?? []);
|
|
15
|
+
let threads = [];
|
|
16
|
+
if (ids.size) {
|
|
17
|
+
let db;
|
|
18
|
+
try {
|
|
19
|
+
db = new DatabaseSync(path.join(this.codexHome, "state_5.sqlite"), { readOnly: true });
|
|
20
|
+
threads = db.prepare("SELECT id, title, name FROM threads WHERE archived = 0 ORDER BY updated_at DESC").all().filter(t => ids.has(t.id)).map(t => ({ id: t.id, title: t.name || t.title || t.id }));
|
|
21
|
+
} catch (error) { if (!/unable to open database/i.test(error.message)) throw error; this.status={available:false,reason:"thread-database-unavailable"}; }
|
|
22
|
+
finally { db?.close(); }
|
|
23
|
+
}
|
|
24
|
+
return { projects, threads };
|
|
25
|
+
}
|
|
26
|
+
}
|