minecodex 0.1.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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/apps/dashboard/app.js +387 -0
- package/apps/dashboard/favicon.svg +18 -0
- package/apps/dashboard/index.html +70 -0
- package/apps/dashboard/styles.css +297 -0
- package/features/images/README.md +106 -0
- package/features/images/codex-feature.json +24 -0
- package/features/images/src/http-server.mjs +302 -0
- package/features/images/src/image-library.mjs +582 -0
- package/features/images/src/main.mjs +52 -0
- package/features/images/src/prompt-index.mjs +182 -0
- package/features/images/src/save-as.mjs +40 -0
- package/features/images/src/thread-metadata.mjs +93 -0
- package/features/images/web/app.js +1045 -0
- package/features/images/web/index.html +162 -0
- package/features/images/web/styles.css +994 -0
- package/features/model-slider/README.md +30 -0
- package/features/model-slider/THIRD_PARTY_NOTICES.md +24 -0
- package/features/model-slider/assets/providers/anthropic.svg +1 -0
- package/features/model-slider/assets/providers/deepseek.svg +1 -0
- package/features/model-slider/assets/providers/gemini.svg +1 -0
- package/features/model-slider/assets/providers/grok.svg +1 -0
- package/features/model-slider/assets/providers/hunyuan.svg +1 -0
- package/features/model-slider/assets/providers/kimi.svg +1 -0
- package/features/model-slider/assets/providers/minimax.svg +1 -0
- package/features/model-slider/assets/providers/openai.svg +1 -0
- package/features/model-slider/assets/providers/qwen.svg +1 -0
- package/features/model-slider/assets/providers/xiaomimimo.svg +1 -0
- package/features/model-slider/assets/providers/zhipu.svg +1 -0
- package/features/model-slider/assets/ui/sparkles.svg +18 -0
- package/features/model-slider/assets/ui/star-off.svg +17 -0
- package/features/model-slider/assets/ui/star.svg +15 -0
- package/features/model-slider/codex-feature.json +56 -0
- package/features/model-slider/vendor/cc-switch-LICENSE +21 -0
- package/features/model-slider/vendor/lucide-LICENSE +35 -0
- package/features/notes/README.md +106 -0
- package/features/notes/assets/icons/circle.svg +15 -0
- package/features/notes/assets/icons/ellipsis.svg +17 -0
- package/features/notes/assets/icons/external-link.svg +17 -0
- package/features/notes/assets/icons/list-todo.svg +19 -0
- package/features/notes/assets/icons/message-square-quote.svg +17 -0
- package/features/notes/assets/icons/paperclip.svg +15 -0
- package/features/notes/assets/icons/star-off.svg +17 -0
- package/features/notes/assets/icons/star.svg +15 -0
- package/features/notes/assets/icons/sticky-note.svg +16 -0
- package/features/notes/assets/icons/trash-2.svg +19 -0
- package/features/notes/codex-feature.json +90 -0
- package/features/notes/src/http-server.mjs +541 -0
- package/features/notes/src/main.mjs +54 -0
- package/features/notes/src/product.mjs +771 -0
- package/features/notes/web/app.js +1495 -0
- package/features/notes/web/codex-artifact-document.svg +1 -0
- package/features/notes/web/codex-attachment.svg +1 -0
- package/features/notes/web/codex-build.svg +1 -0
- package/features/notes/web/codex-code.svg +1 -0
- package/features/notes/web/codex-cplusplus.svg +1 -0
- package/features/notes/web/codex-css.svg +1 -0
- package/features/notes/web/codex-document.svg +1 -0
- package/features/notes/web/codex-file.svg +1 -0
- package/features/notes/web/codex-folder.svg +1 -0
- package/features/notes/web/codex-hashes.svg +1 -0
- package/features/notes/web/codex-html.svg +1 -0
- package/features/notes/web/codex-image.svg +1 -0
- package/features/notes/web/codex-java.svg +1 -0
- package/features/notes/web/codex-javascript.svg +1 -0
- package/features/notes/web/codex-json.svg +1 -0
- package/features/notes/web/codex-notebook.svg +1 -0
- package/features/notes/web/codex-pdf.svg +1 -0
- package/features/notes/web/codex-pencil.svg +3 -0
- package/features/notes/web/codex-php.svg +1 -0
- package/features/notes/web/codex-plus.svg +3 -0
- package/features/notes/web/codex-presentation.svg +1 -0
- package/features/notes/web/codex-python.svg +1 -0
- package/features/notes/web/codex-react.svg +1 -0
- package/features/notes/web/codex-rust.svg +1 -0
- package/features/notes/web/codex-shell.svg +1 -0
- package/features/notes/web/codex-skill.svg +1 -0
- package/features/notes/web/codex-spreadsheet.svg +1 -0
- package/features/notes/web/codex-task.svg +3 -0
- package/features/notes/web/codex-terminal.svg +1 -0
- package/features/notes/web/codex-toml.svg +1 -0
- package/features/notes/web/codex-typescript.svg +1 -0
- package/features/notes/web/codex-view-all.svg +3 -0
- package/features/notes/web/index.html +15 -0
- package/features/notes/web/styles.css +1155 -0
- package/package.json +67 -0
- package/packages/cli/bin/mcx.mjs +7 -0
- package/packages/cli/src/browser-exchange.mjs +31 -0
- package/packages/cli/src/commands.mjs +389 -0
- package/packages/cli/src/config.mjs +101 -0
- package/packages/cli/src/control-server.mjs +371 -0
- package/packages/cli/src/npm-adapter.mjs +40 -0
- package/packages/cli/src/paths.mjs +33 -0
- package/packages/cli/src/platform.mjs +368 -0
- package/packages/cli/src/runtime-manager.mjs +338 -0
- package/packages/runtime-host/README.md +171 -0
- package/packages/runtime-host/src/codex-runtime.mjs +4311 -0
- package/packages/runtime-host/src/feature-registry.mjs +827 -0
- package/packages/runtime-host/src/main.mjs +98 -0
- package/packages/runtime-host/src/pid-file.mjs +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vontean
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# MineCodex
|
|
2
|
+
|
|
3
|
+
MineCodex 给 Codex Desktop 增加三个轻量、本地优先的插件:
|
|
4
|
+
|
|
5
|
+
- **Model Slider**:更清楚、更顺滑的模型选择窗口。
|
|
6
|
+
- **Notes**:跨会话管理 Todo、Prompts 和 Files。
|
|
7
|
+
- **Images**:集中浏览生成图片、Prompt 和来源任务。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
MineCodex v1 仅支持 macOS,需要 Node.js 22.5 或更高版本。安装时会从
|
|
12
|
+
`/Applications/ChatGPT.app` 或 `~/Applications/ChatGPT.app` 查找 Codex Desktop;
|
|
13
|
+
如果两处都找不到,安装会停止并给出提示。
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g minecodex
|
|
17
|
+
mcx install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
安装后三个插件默认开启。打开本地控制台:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
mcx open
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
常用命令:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
mcx install
|
|
30
|
+
mcx uninstall [--purge]
|
|
31
|
+
mcx update
|
|
32
|
+
mcx open
|
|
33
|
+
mcx status [--json]
|
|
34
|
+
mcx restart
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
关闭插件只会停止注册,不会删除数据。`mcx uninstall` 默认保留 MineCodex 设置、
|
|
38
|
+
Notes 数据和 Images 索引;只有显式传入 `--purge` 才会删除 MineCodex 自有数据。
|
|
39
|
+
MineCodex 永远不会删除 `~/.codex/generated_images` 中的原图或 Notes 引用的源文件。
|
|
40
|
+
|
|
41
|
+
## 开发
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install
|
|
45
|
+
npm test
|
|
46
|
+
npm run check
|
|
47
|
+
npm run pack:check
|
|
48
|
+
npm run pack:audit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
MineCodex 是唯一发布仓库。三个插件继续拥有各自的数据和页面;RuntimeHost 只负责
|
|
52
|
+
Codex 中的入口、主题与交互适配。
|
|
53
|
+
|
|
54
|
+
MineCodex 是非官方独立项目,与 OpenAI 无关联。项目自有代码以 MIT License 授权;
|
|
55
|
+
第三方图标、品牌和兼容资产遵循各自的权利说明,见
|
|
56
|
+
[`features/model-slider/THIRD_PARTY_NOTICES.md`](features/model-slider/THIRD_PARTY_NOTICES.md)。
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
const locale = navigator.language.toLowerCase().startsWith("zh") ? "zh-CN" : "en";
|
|
2
|
+
document.documentElement.lang = locale;
|
|
3
|
+
|
|
4
|
+
const copy = {
|
|
5
|
+
en: {
|
|
6
|
+
subtitle: "Personal plugins for Codex",
|
|
7
|
+
connecting: "Connecting…",
|
|
8
|
+
runtimeActive: "Active in Codex",
|
|
9
|
+
runtimeStopped: "Runtime unavailable",
|
|
10
|
+
runtimeNotInjected: "Runtime running — restart Codex to apply plugins",
|
|
11
|
+
plugins: "Plugins",
|
|
12
|
+
pluginsCopy: "Choose which enhancements appear in Codex.",
|
|
13
|
+
restartTitle: "Restart Codex to apply changes",
|
|
14
|
+
restartCopy: "Your plugin choices are saved. Restart when you are ready.",
|
|
15
|
+
restart: "Restart Codex",
|
|
16
|
+
restarting: "Restarting…",
|
|
17
|
+
saved: "Saved",
|
|
18
|
+
saving: "Saving…",
|
|
19
|
+
states: {
|
|
20
|
+
saved: "Saved — restart Codex to apply",
|
|
21
|
+
configured: "Configured — starting in Codex…",
|
|
22
|
+
launching: "Launching in Codex…",
|
|
23
|
+
injected: "Active in Codex",
|
|
24
|
+
healthy: "Healthy in Codex",
|
|
25
|
+
failed: "Failed — restart Codex",
|
|
26
|
+
stopped: "Stopped",
|
|
27
|
+
"restart-required": "Restart required — restart Codex",
|
|
28
|
+
},
|
|
29
|
+
loadError: "Couldn’t load MineCodex",
|
|
30
|
+
loadErrorCopy: "Check that the background service is running.",
|
|
31
|
+
retry: "Try again",
|
|
32
|
+
version: "Version",
|
|
33
|
+
installation: "Installation",
|
|
34
|
+
runtimeHost: "RuntimeHost",
|
|
35
|
+
running: "Running",
|
|
36
|
+
stopped: "Stopped",
|
|
37
|
+
checkUpdates: "Check for updates",
|
|
38
|
+
checkingUpdates: "Checking…",
|
|
39
|
+
current: "You’re up to date.",
|
|
40
|
+
updateAvailable: (version) => `MineCodex ${version} is available. Run mcx update in Terminal.`,
|
|
41
|
+
changeFailed: "The plugin setting could not be saved.",
|
|
42
|
+
restartFailed: "MineCodex could not restart Codex. Check the service log and try again.",
|
|
43
|
+
failureDetail: "Health or renderer status could not be confirmed.",
|
|
44
|
+
},
|
|
45
|
+
"zh-CN": {
|
|
46
|
+
subtitle: "为 Codex 准备的个人插件",
|
|
47
|
+
connecting: "正在连接…",
|
|
48
|
+
runtimeActive: "已在 Codex 中生效",
|
|
49
|
+
runtimeStopped: "运行服务不可用",
|
|
50
|
+
runtimeNotInjected: "运行服务已启动——重启 Codex 以应用插件",
|
|
51
|
+
plugins: "插件",
|
|
52
|
+
pluginsCopy: "选择要在 Codex 中启用的增强功能。",
|
|
53
|
+
restartTitle: "重启 Codex 以应用更改",
|
|
54
|
+
restartCopy: "插件选择已经保存,可以在准备好后统一重启。",
|
|
55
|
+
restart: "重启 Codex",
|
|
56
|
+
restarting: "正在重启…",
|
|
57
|
+
saved: "已保存",
|
|
58
|
+
saving: "正在保存…",
|
|
59
|
+
states: {
|
|
60
|
+
saved: "已保存——重启 Codex 后应用",
|
|
61
|
+
configured: "已配置——正在 Codex 中启动…",
|
|
62
|
+
launching: "正在 Codex 中启动…",
|
|
63
|
+
injected: "已在 Codex 中生效",
|
|
64
|
+
healthy: "Codex 中运行正常",
|
|
65
|
+
failed: "运行失败——请重启 Codex",
|
|
66
|
+
stopped: "已停止",
|
|
67
|
+
"restart-required": "需要重启——请重启 Codex",
|
|
68
|
+
},
|
|
69
|
+
loadError: "无法加载 MineCodex",
|
|
70
|
+
loadErrorCopy: "请检查后台服务是否正在运行。",
|
|
71
|
+
retry: "重试",
|
|
72
|
+
version: "版本",
|
|
73
|
+
installation: "安装位置",
|
|
74
|
+
runtimeHost: "运行服务",
|
|
75
|
+
running: "运行中",
|
|
76
|
+
stopped: "已停止",
|
|
77
|
+
checkUpdates: "检查更新",
|
|
78
|
+
checkingUpdates: "正在检查…",
|
|
79
|
+
current: "当前已是最新版本。",
|
|
80
|
+
updateAvailable: (version) => `发现 MineCodex ${version},请在终端运行 mcx update。`,
|
|
81
|
+
changeFailed: "插件设置保存失败。",
|
|
82
|
+
restartFailed: "MineCodex 无法重启 Codex。请检查服务日志后重试。",
|
|
83
|
+
failureDetail: "无法确认插件健康状态或 Codex 渲染器状态。",
|
|
84
|
+
},
|
|
85
|
+
}[locale];
|
|
86
|
+
|
|
87
|
+
const elements = {
|
|
88
|
+
subtitle: document.querySelector("#subtitle"),
|
|
89
|
+
runtimeDot: document.querySelector("#runtime-dot"),
|
|
90
|
+
runtimeLabel: document.querySelector("#runtime-label"),
|
|
91
|
+
restartNotice: document.querySelector("#restart-notice"),
|
|
92
|
+
restartTitle: document.querySelector("#restart-title"),
|
|
93
|
+
restartCopy: document.querySelector("#restart-copy"),
|
|
94
|
+
restartButton: document.querySelector("#restart-button"),
|
|
95
|
+
pluginsHeading: document.querySelector("#plugins-heading"),
|
|
96
|
+
pluginsCopy: document.querySelector("#plugins-copy"),
|
|
97
|
+
saveState: document.querySelector("#save-state"),
|
|
98
|
+
pluginList: document.querySelector("#plugin-list"),
|
|
99
|
+
loadError: document.querySelector("#load-error"),
|
|
100
|
+
errorTitle: document.querySelector("#error-title"),
|
|
101
|
+
errorCopy: document.querySelector("#error-copy"),
|
|
102
|
+
retryButton: document.querySelector("#retry-button"),
|
|
103
|
+
versionLabel: document.querySelector("#version-label"),
|
|
104
|
+
versionValue: document.querySelector("#version-value"),
|
|
105
|
+
installLabel: document.querySelector("#install-label"),
|
|
106
|
+
hostLabel: document.querySelector("#host-label"),
|
|
107
|
+
hostValue: document.querySelector("#host-value"),
|
|
108
|
+
updateButton: document.querySelector("#update-button"),
|
|
109
|
+
toast: document.querySelector("#toast"),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let state = null;
|
|
113
|
+
let toastTimer = null;
|
|
114
|
+
const pendingFeatureIds = new Set();
|
|
115
|
+
|
|
116
|
+
function applyStaticCopy() {
|
|
117
|
+
elements.subtitle.textContent = copy.subtitle;
|
|
118
|
+
elements.runtimeLabel.textContent = copy.connecting;
|
|
119
|
+
elements.restartTitle.textContent = copy.restartTitle;
|
|
120
|
+
elements.restartCopy.textContent = copy.restartCopy;
|
|
121
|
+
elements.restartButton.querySelector(".button-label").textContent = copy.restart;
|
|
122
|
+
elements.pluginsHeading.textContent = copy.plugins;
|
|
123
|
+
elements.pluginsCopy.textContent = copy.pluginsCopy;
|
|
124
|
+
elements.errorTitle.textContent = copy.loadError;
|
|
125
|
+
elements.errorCopy.textContent = copy.loadErrorCopy;
|
|
126
|
+
elements.retryButton.textContent = copy.retry;
|
|
127
|
+
elements.versionLabel.textContent = copy.version;
|
|
128
|
+
elements.installLabel.textContent = copy.installation;
|
|
129
|
+
elements.hostLabel.textContent = copy.runtimeHost;
|
|
130
|
+
elements.updateButton.textContent = copy.checkUpdates;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function featureState(feature) {
|
|
134
|
+
const stateName = feature.state
|
|
135
|
+
?? (feature.failure ? "failed" : feature.healthy ? "healthy" : feature.injected ? "injected" : feature.applied ? "launching" : feature.enabled ? "restart-required" : "stopped");
|
|
136
|
+
const label = copy.states[stateName] ?? copy.states.stopped;
|
|
137
|
+
const className = {
|
|
138
|
+
healthy: "is-active",
|
|
139
|
+
injected: "is-active",
|
|
140
|
+
configured: "is-warning",
|
|
141
|
+
failed: "is-failed",
|
|
142
|
+
launching: "is-warning",
|
|
143
|
+
"restart-required": "is-warning",
|
|
144
|
+
saved: "is-warning",
|
|
145
|
+
stopped: "is-stopped",
|
|
146
|
+
}[stateName] ?? "is-warning";
|
|
147
|
+
return {
|
|
148
|
+
label: feature.failure?.message && stateName === "failed" ? `${label}: ${copy.failureDetail}` : label,
|
|
149
|
+
className,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function element(tag, className) {
|
|
154
|
+
const node = document.createElement(tag);
|
|
155
|
+
if (className) node.className = className;
|
|
156
|
+
return node;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createPluginCard(feature) {
|
|
160
|
+
const card = element("article", "plugin-card");
|
|
161
|
+
card.dataset.featureId = feature.id;
|
|
162
|
+
|
|
163
|
+
const icon = element("div", "plugin-icon");
|
|
164
|
+
icon.setAttribute("aria-hidden", "true");
|
|
165
|
+
const image = element("img");
|
|
166
|
+
image.alt = "";
|
|
167
|
+
image.width = 18;
|
|
168
|
+
image.height = 18;
|
|
169
|
+
icon.append(image);
|
|
170
|
+
|
|
171
|
+
const pluginCopy = element("div", "plugin-copy");
|
|
172
|
+
const titleLine = element("div", "plugin-title-line");
|
|
173
|
+
const name = element("h3", "plugin-name");
|
|
174
|
+
const version = element("span", "plugin-version");
|
|
175
|
+
titleLine.append(name, version);
|
|
176
|
+
const description = element("p", "plugin-description");
|
|
177
|
+
pluginCopy.append(titleLine, description);
|
|
178
|
+
|
|
179
|
+
const control = element("div", "plugin-control");
|
|
180
|
+
const label = element("label", "switch");
|
|
181
|
+
const input = element("input");
|
|
182
|
+
input.type = "checkbox";
|
|
183
|
+
input.name = `feature-${feature.id}`;
|
|
184
|
+
const track = element("span", "switch-track");
|
|
185
|
+
track.setAttribute("aria-hidden", "true");
|
|
186
|
+
label.append(input, track);
|
|
187
|
+
const pluginState = element("span", "plugin-state");
|
|
188
|
+
const dot = element("span", "status-dot");
|
|
189
|
+
dot.setAttribute("aria-hidden", "true");
|
|
190
|
+
const stateLabel = element("span", "plugin-state-label");
|
|
191
|
+
pluginState.append(dot, stateLabel);
|
|
192
|
+
control.append(label, pluginState);
|
|
193
|
+
card.append(icon, pluginCopy, control);
|
|
194
|
+
return card;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function updatePluginCard(card, feature) {
|
|
198
|
+
const status = featureState(feature);
|
|
199
|
+
const input = card.querySelector('input[type="checkbox"]');
|
|
200
|
+
const image = card.querySelector(".plugin-icon img");
|
|
201
|
+
const name = card.querySelector(".plugin-name");
|
|
202
|
+
const version = card.querySelector(".plugin-version");
|
|
203
|
+
const description = card.querySelector(".plugin-description");
|
|
204
|
+
const dot = card.querySelector(".plugin-state .status-dot");
|
|
205
|
+
const stateLabel = card.querySelector(".plugin-state-label");
|
|
206
|
+
card.dataset.featureId = feature.id;
|
|
207
|
+
image.src = feature.iconUrl;
|
|
208
|
+
name.textContent = feature.label;
|
|
209
|
+
version.textContent = feature.version;
|
|
210
|
+
description.textContent = feature.description;
|
|
211
|
+
input.checked = Boolean(feature.enabled);
|
|
212
|
+
input.disabled = pendingFeatureIds.has(feature.id);
|
|
213
|
+
input.setAttribute("aria-label", feature.label);
|
|
214
|
+
dot.className = `status-dot ${status.className}`;
|
|
215
|
+
stateLabel.textContent = status.label;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function findPluginCard(featureId) {
|
|
219
|
+
return [...elements.pluginList.children].find((child) => child.dataset.featureId === featureId) ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderPluginCards(features) {
|
|
223
|
+
for (const child of [...elements.pluginList.children]) {
|
|
224
|
+
if (!child.dataset.featureId) child.remove();
|
|
225
|
+
}
|
|
226
|
+
const featureIds = new Set(features.map(({ id }) => id));
|
|
227
|
+
for (const feature of features) {
|
|
228
|
+
let card = findPluginCard(feature.id);
|
|
229
|
+
if (!card) {
|
|
230
|
+
card = createPluginCard(feature);
|
|
231
|
+
elements.pluginList.append(card);
|
|
232
|
+
}
|
|
233
|
+
updatePluginCard(card, feature);
|
|
234
|
+
}
|
|
235
|
+
for (const card of [...elements.pluginList.children]) {
|
|
236
|
+
if (card.dataset.featureId && !featureIds.has(card.dataset.featureId)) card.remove();
|
|
237
|
+
}
|
|
238
|
+
elements.pluginList.setAttribute("aria-busy", "false");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderRuntime() {
|
|
242
|
+
const runtimeRunning = Boolean(state?.runtime?.running);
|
|
243
|
+
const renderer = state?.renderer;
|
|
244
|
+
const rendererCurrent = Boolean(
|
|
245
|
+
renderer?.discovered
|
|
246
|
+
&& renderer.runtimeSessionId
|
|
247
|
+
&& renderer.runtimeSessionId === state?.runtime?.runtimeSessionId
|
|
248
|
+
&& renderer.active
|
|
249
|
+
&& Number(renderer.injectedTargets) > 0,
|
|
250
|
+
);
|
|
251
|
+
const active = runtimeRunning && rendererCurrent;
|
|
252
|
+
elements.runtimeDot.className = `status-dot ${active ? "is-active" : "is-warning"}`;
|
|
253
|
+
elements.runtimeLabel.textContent = active
|
|
254
|
+
? copy.runtimeActive
|
|
255
|
+
: runtimeRunning
|
|
256
|
+
? copy.runtimeNotInjected
|
|
257
|
+
: copy.runtimeStopped;
|
|
258
|
+
elements.hostValue.textContent = runtimeRunning ? copy.running : copy.stopped;
|
|
259
|
+
elements.versionValue.textContent = state?.version ?? "—";
|
|
260
|
+
elements.restartNotice.hidden = !state?.pendingRestart;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function render() {
|
|
264
|
+
renderRuntime();
|
|
265
|
+
renderPluginCards(state?.features ?? []);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function captureFocus(featureId) {
|
|
269
|
+
const active = document.activeElement;
|
|
270
|
+
const card = active?.closest?.("[data-feature-id]");
|
|
271
|
+
if (!card || card.dataset.featureId !== featureId) return null;
|
|
272
|
+
return { featureId, input: active.matches?.('input[type="checkbox"]') };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function restoreFocus(focus) {
|
|
276
|
+
if (!focus?.input) return;
|
|
277
|
+
const card = findPluginCard(focus.featureId);
|
|
278
|
+
card?.querySelector('input[type="checkbox"]')?.focus();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function showToast(message) {
|
|
282
|
+
clearTimeout(toastTimer);
|
|
283
|
+
elements.toast.textContent = message;
|
|
284
|
+
elements.toast.hidden = false;
|
|
285
|
+
toastTimer = setTimeout(() => { elements.toast.hidden = true; }, 4200);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function replaceFeatureInState(featureId, patch) {
|
|
289
|
+
state.features = state.features.map((feature) => feature.id === featureId ? { ...feature, ...patch } : feature);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function load() {
|
|
293
|
+
elements.loadError.hidden = true;
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch("/api/status", { headers: { "Accept-Language": locale } });
|
|
296
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
297
|
+
state = await response.json();
|
|
298
|
+
elements.pluginList.hidden = false;
|
|
299
|
+
render();
|
|
300
|
+
} catch {
|
|
301
|
+
elements.pluginList.hidden = true;
|
|
302
|
+
elements.loadError.hidden = false;
|
|
303
|
+
elements.runtimeDot.className = "status-dot is-warning";
|
|
304
|
+
elements.runtimeLabel.textContent = copy.runtimeStopped;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
elements.pluginList.addEventListener("change", async (event) => {
|
|
309
|
+
const input = event.target.closest('input[type="checkbox"]');
|
|
310
|
+
const card = input?.closest("[data-feature-id]");
|
|
311
|
+
if (!input || !card || !state) return;
|
|
312
|
+
const featureId = card.dataset.featureId;
|
|
313
|
+
const previousFeature = { ...state.features.find((feature) => feature.id === featureId) };
|
|
314
|
+
const focus = captureFocus(featureId);
|
|
315
|
+
pendingFeatureIds.add(featureId);
|
|
316
|
+
replaceFeatureInState(featureId, { enabled: input.checked, state: "saved" });
|
|
317
|
+
updatePluginCard(card, state.features.find((feature) => feature.id === featureId));
|
|
318
|
+
elements.saveState.textContent = copy.saving;
|
|
319
|
+
try {
|
|
320
|
+
const response = await fetch(`/api/features/${encodeURIComponent(featureId)}`, {
|
|
321
|
+
method: "PATCH",
|
|
322
|
+
headers: { "Content-Type": "application/json", "Accept-Language": locale },
|
|
323
|
+
body: JSON.stringify({ enabled: input.checked }),
|
|
324
|
+
});
|
|
325
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
326
|
+
const result = await response.json();
|
|
327
|
+
const changedFeature = result.features?.find((feature) => feature.id === featureId);
|
|
328
|
+
if (changedFeature) replaceFeatureInState(featureId, changedFeature);
|
|
329
|
+
state.pendingRestart = result.pendingRestart;
|
|
330
|
+
pendingFeatureIds.delete(featureId);
|
|
331
|
+
const updatedFeature = changedFeature ?? state.features.find((feature) => feature.id === featureId);
|
|
332
|
+
if (updatedFeature) updatePluginCard(card, updatedFeature);
|
|
333
|
+
renderRuntime();
|
|
334
|
+
restoreFocus(focus);
|
|
335
|
+
elements.saveState.textContent = pendingFeatureIds.size ? copy.saving : copy.saved;
|
|
336
|
+
} catch {
|
|
337
|
+
pendingFeatureIds.delete(featureId);
|
|
338
|
+
replaceFeatureInState(featureId, previousFeature);
|
|
339
|
+
updatePluginCard(card, previousFeature);
|
|
340
|
+
elements.saveState.textContent = pendingFeatureIds.size ? copy.saving : "";
|
|
341
|
+
restoreFocus(focus);
|
|
342
|
+
showToast(copy.changeFailed);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
elements.restartButton.addEventListener("click", async () => {
|
|
347
|
+
elements.restartButton.disabled = true;
|
|
348
|
+
elements.restartButton.querySelector(".button-label").textContent = copy.restarting;
|
|
349
|
+
try {
|
|
350
|
+
const response = await fetch("/api/restart", {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "Content-Type": "application/json", "Accept-Language": locale },
|
|
353
|
+
body: "{}",
|
|
354
|
+
});
|
|
355
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
356
|
+
await load();
|
|
357
|
+
} catch {
|
|
358
|
+
showToast(copy.restartFailed);
|
|
359
|
+
} finally {
|
|
360
|
+
elements.restartButton.disabled = false;
|
|
361
|
+
elements.restartButton.querySelector(".button-label").textContent = copy.restart;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
elements.retryButton.addEventListener("click", () => {
|
|
366
|
+
elements.pluginList.hidden = false;
|
|
367
|
+
void load();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
elements.updateButton.addEventListener("click", async () => {
|
|
371
|
+
elements.updateButton.disabled = true;
|
|
372
|
+
elements.updateButton.textContent = copy.checkingUpdates;
|
|
373
|
+
try {
|
|
374
|
+
const response = await fetch("/api/update");
|
|
375
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
376
|
+
const update = await response.json();
|
|
377
|
+
showToast(update.available ? copy.updateAvailable(update.latest) : copy.current);
|
|
378
|
+
} catch {
|
|
379
|
+
showToast(copy.loadErrorCopy);
|
|
380
|
+
} finally {
|
|
381
|
+
elements.updateButton.disabled = false;
|
|
382
|
+
elements.updateButton.textContent = copy.checkUpdates;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
applyStaticCopy();
|
|
387
|
+
void load();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.30.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-sparkles"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" />
|
|
15
|
+
<path d="M20 2v4" />
|
|
16
|
+
<path d="M22 4h-4" />
|
|
17
|
+
<circle cx="4" cy="20" r="2" />
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>MineCodex</title>
|
|
8
|
+
<link rel="icon" href="/favicon.svg?v=0.1.0" type="image/svg+xml">
|
|
9
|
+
<link rel="stylesheet" href="/styles.css?v=0.1.0">
|
|
10
|
+
<script type="module" src="/app.js?v=0.1.0"></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main class="shell">
|
|
14
|
+
<header class="page-header">
|
|
15
|
+
<div>
|
|
16
|
+
<h1>MineCodex</h1>
|
|
17
|
+
<p id="subtitle" class="subtitle">Personal plugins for Codex</p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="runtime-state" aria-live="polite">
|
|
20
|
+
<span id="runtime-dot" class="status-dot" aria-hidden="true"></span>
|
|
21
|
+
<span id="runtime-label">Connecting…</span>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<section id="restart-notice" class="restart-notice" hidden aria-live="polite">
|
|
26
|
+
<div>
|
|
27
|
+
<h2 id="restart-title">Restart Codex to apply changes</h2>
|
|
28
|
+
<p id="restart-copy">Your plugin choices are saved. Restart when you are ready.</p>
|
|
29
|
+
</div>
|
|
30
|
+
<button id="restart-button" class="button primary" type="button">
|
|
31
|
+
<span class="button-label">Restart Codex</span>
|
|
32
|
+
</button>
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<section aria-labelledby="plugins-heading">
|
|
36
|
+
<div class="section-heading">
|
|
37
|
+
<div>
|
|
38
|
+
<h2 id="plugins-heading">Plugins</h2>
|
|
39
|
+
<p id="plugins-copy">Choose which enhancements appear in Codex.</p>
|
|
40
|
+
</div>
|
|
41
|
+
<span id="save-state" class="save-state" aria-live="polite"></span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div id="plugin-list" class="plugin-list" aria-busy="true">
|
|
45
|
+
<div class="plugin-skeleton"></div>
|
|
46
|
+
<div class="plugin-skeleton"></div>
|
|
47
|
+
<div class="plugin-skeleton"></div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div id="load-error" class="error-state" hidden role="alert">
|
|
51
|
+
<div>
|
|
52
|
+
<strong id="error-title">Couldn’t load MineCodex</strong>
|
|
53
|
+
<p id="error-copy">Check that the background service is running.</p>
|
|
54
|
+
</div>
|
|
55
|
+
<button id="retry-button" class="button secondary" type="button">Try again</button>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<footer class="diagnostics">
|
|
60
|
+
<dl>
|
|
61
|
+
<div><dt id="version-label">Version</dt><dd id="version-value">—</dd></div>
|
|
62
|
+
<div><dt id="install-label">Installation</dt><dd>~/Library/Application Support/MineCodex</dd></div>
|
|
63
|
+
<div><dt id="host-label">RuntimeHost</dt><dd id="host-value">—</dd></div>
|
|
64
|
+
</dl>
|
|
65
|
+
<button id="update-button" class="button quiet" type="button">Check for updates</button>
|
|
66
|
+
</footer>
|
|
67
|
+
</main>
|
|
68
|
+
<div id="toast" class="toast" hidden role="status" aria-live="polite"></div>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|