@wu529778790/open-im 1.6.5 → 1.6.6-beta.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/README.md +13 -0
- package/README.zh-CN.md +13 -0
- package/dist/config-web-page-i18n.d.ts +10 -0
- package/dist/config-web-page-i18n.js +10 -0
- package/dist/config-web-page-script.js +36 -0
- package/dist/config-web-page-template.js +71 -22
- package/dist/config-web-page.test.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,19 @@ Multi-platform IM bridge for AI CLI tools. Connect Telegram, Feishu, WeCom, Ding
|
|
|
14
14
|
- Isolated sessions: each user gets an independent local session, and `/new` resets it
|
|
15
15
|
- Built-in commands: `/help`, `/new`, `/cd`, `/pwd`, `/status`
|
|
16
16
|
|
|
17
|
+
## Coverage Matrix
|
|
18
|
+
|
|
19
|
+
Capability levels: `Native` = fully supported in-channel, `Fallback` = degraded behavior or text fallback, `None` = not currently supported.
|
|
20
|
+
|
|
21
|
+
| Platform | Text Inbound | Image Inbound | File Inbound | Voice Inbound | Video Inbound | Streaming Reply | Image Reply | Card Reply |
|
|
22
|
+
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
|
23
|
+
| Telegram | Native | Native | Native | Native | Native | Native | Native | Native |
|
|
24
|
+
| Feishu | Native | Native | Native | Fallback | Fallback | Native | Native | Native |
|
|
25
|
+
| QQ | Native | Fallback | Fallback | Fallback | Fallback | None | Fallback | Fallback |
|
|
26
|
+
| WeCom | Native | Fallback | Fallback | Fallback | Fallback | Native | Native | Native |
|
|
27
|
+
| DingTalk | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
28
|
+
| WeChat (experimental) | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
29
|
+
|
|
17
30
|
## Requirements
|
|
18
31
|
|
|
19
32
|
- Node.js >= 20
|
package/README.zh-CN.md
CHANGED
|
@@ -14,6 +14,19 @@
|
|
|
14
14
|
- 会话隔离:每个用户独立维护本地会话,`/new` 可重置
|
|
15
15
|
- 常用命令:支持 `/help`、`/new`、`/cd`、`/pwd`、`/status`
|
|
16
16
|
|
|
17
|
+
## 覆盖矩阵
|
|
18
|
+
|
|
19
|
+
能力等级说明:`Native` 表示平台内原生支持,`Fallback` 表示有降级方案或文本兜底,`None` 表示当前暂不支持。
|
|
20
|
+
|
|
21
|
+
| 平台 | 文本输入 | 图片输入 | 文件输入 | 语音输入 | 视频输入 | 流式回复 | 图片回复 | 卡片回复 |
|
|
22
|
+
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
|
23
|
+
| Telegram | Native | Native | Native | Native | Native | Native | Native | Native |
|
|
24
|
+
| 飞书 | Native | Native | Native | Fallback | Fallback | Native | Native | Native |
|
|
25
|
+
| QQ | Native | Fallback | Fallback | Fallback | Fallback | None | Fallback | Fallback |
|
|
26
|
+
| 企业微信 | Native | Fallback | Fallback | Fallback | Fallback | Native | Native | Native |
|
|
27
|
+
| 钉钉 | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
28
|
+
| 微信(测试中) | Native | Fallback | Fallback | Fallback | Fallback | Native | Fallback | Native |
|
|
29
|
+
|
|
17
30
|
## 环境要求
|
|
18
31
|
|
|
19
32
|
- Node.js >= 20
|
|
@@ -65,6 +65,11 @@ export declare const PAGE_TEXTS: {
|
|
|
65
65
|
readonly aiTitle: "AI Tooling";
|
|
66
66
|
readonly aiHint: "";
|
|
67
67
|
readonly claudeNote: "Claude credentials are still read from environment variables or ~/.claude/settings.json. This page manages local bridge config, not Claude account auth.";
|
|
68
|
+
readonly aiCommonTitle: "Shared defaults";
|
|
69
|
+
readonly aiCommonHint: "Choose the default AI tool first. Tool-specific fields are shown below.";
|
|
70
|
+
readonly aiToolConfigTitle: "Tool-specific settings";
|
|
71
|
+
readonly aiToolConfigHint: "Only the selected tool's fields are shown.";
|
|
72
|
+
readonly aiConfigSummary: "Editing {tool} settings. Switch tools above if you need to configure another runtime.";
|
|
68
73
|
readonly aiTool: "Default AI tool";
|
|
69
74
|
readonly workDir: "Default work directory";
|
|
70
75
|
readonly claudeCli: "Claude CLI path";
|
|
@@ -164,6 +169,11 @@ export declare const PAGE_TEXTS: {
|
|
|
164
169
|
readonly aiTitle: "AI 工具配置";
|
|
165
170
|
readonly aiHint: "";
|
|
166
171
|
readonly claudeNote: "Claude 凭证仍然从环境变量或 ~/.claude/settings.json 读取。这个页面只管理本地桥接配置,不负责 Claude 账号登录。";
|
|
172
|
+
readonly aiCommonTitle: "公共默认配置";
|
|
173
|
+
readonly aiCommonHint: "先选择默认 AI 工具,下方再显示对应的工具专属配置。";
|
|
174
|
+
readonly aiToolConfigTitle: "工具专属配置";
|
|
175
|
+
readonly aiToolConfigHint: "只显示当前选中 AI 工具的字段。";
|
|
176
|
+
readonly aiConfigSummary: "正在编辑 {tool} 的配置,如需配置其他工具,可在上方切换。";
|
|
167
177
|
readonly aiTool: "默认 AI 工具";
|
|
168
178
|
readonly workDir: "默认工作目录";
|
|
169
179
|
readonly claudeCli: "Claude CLI 路径";
|
|
@@ -65,6 +65,11 @@ export const PAGE_TEXTS = {
|
|
|
65
65
|
aiTitle: "AI Tooling",
|
|
66
66
|
aiHint: "",
|
|
67
67
|
claudeNote: "Claude credentials are still read from environment variables or ~/.claude/settings.json. This page manages local bridge config, not Claude account auth.",
|
|
68
|
+
aiCommonTitle: "Shared defaults",
|
|
69
|
+
aiCommonHint: "Choose the default AI tool first. Tool-specific fields are shown below.",
|
|
70
|
+
aiToolConfigTitle: "Tool-specific settings",
|
|
71
|
+
aiToolConfigHint: "Only the selected tool's fields are shown.",
|
|
72
|
+
aiConfigSummary: "Editing {tool} settings. Switch tools above if you need to configure another runtime.",
|
|
68
73
|
aiTool: "Default AI tool",
|
|
69
74
|
workDir: "Default work directory",
|
|
70
75
|
claudeCli: "Claude CLI path",
|
|
@@ -164,6 +169,11 @@ export const PAGE_TEXTS = {
|
|
|
164
169
|
aiTitle: "AI \u5de5\u5177\u914d\u7f6e",
|
|
165
170
|
aiHint: "",
|
|
166
171
|
claudeNote: "Claude \u51ed\u8bc1\u4ecd\u7136\u4ece\u73af\u5883\u53d8\u91cf\u6216 ~/.claude/settings.json \u8bfb\u53d6\u3002\u8fd9\u4e2a\u9875\u9762\u53ea\u7ba1\u7406\u672c\u5730\u6865\u63a5\u914d\u7f6e\uff0c\u4e0d\u8d1f\u8d23 Claude \u8d26\u53f7\u767b\u5f55\u3002",
|
|
172
|
+
aiCommonTitle: "\u516c\u5171\u9ed8\u8ba4\u914d\u7f6e",
|
|
173
|
+
aiCommonHint: "\u5148\u9009\u62e9\u9ed8\u8ba4 AI \u5de5\u5177\uff0c\u4e0b\u65b9\u518d\u663e\u793a\u5bf9\u5e94\u7684\u5de5\u5177\u4e13\u5c5e\u914d\u7f6e\u3002",
|
|
174
|
+
aiToolConfigTitle: "\u5de5\u5177\u4e13\u5c5e\u914d\u7f6e",
|
|
175
|
+
aiToolConfigHint: "\u53ea\u663e\u793a\u5f53\u524d\u9009\u4e2d AI \u5de5\u5177\u7684\u5b57\u6bb5\u3002",
|
|
176
|
+
aiConfigSummary: "\u6b63\u5728\u7f16\u8f91 {tool} \u7684\u914d\u7f6e\uff0c\u5982\u9700\u914d\u7f6e\u5176\u4ed6\u5de5\u5177\uff0c\u53ef\u5728\u4e0a\u65b9\u5207\u6362\u3002",
|
|
167
177
|
aiTool: "\u9ed8\u8ba4 AI \u5de5\u5177",
|
|
168
178
|
workDir: "\u9ed8\u8ba4\u5de5\u4f5c\u76ee\u5f55",
|
|
169
179
|
claudeCli: "Claude CLI \u8def\u5f84",
|
|
@@ -6,6 +6,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
6
6
|
{ key: "dingtalk", label: "DingTalk", fields: ["aiCommand", "clientId", "clientSecret", "cardTemplateId", "allowedUserIds"], testFields: ["clientId", "clientSecret"] },
|
|
7
7
|
];
|
|
8
8
|
const platformKeys = platformDefinitions.map((platform) => platform.key);
|
|
9
|
+
const aiTools = ["claude", "codex", "cursor", "codebuddy"];
|
|
9
10
|
const ids = platformDefinitions.flatMap((platform) => ["enabled", ...platform.fields].map((field) => platform.key + "-" + field)).concat(["ai-aiCommand","ai-claudeCliPath","ai-claudeWorkDir","ai-claudeSkipPermissions","ai-claudeTimeoutMs","ai-codexTimeoutMs","ai-codebuddyTimeoutMs","ai-claudeModel","ai-cursorCliPath","ai-codexCliPath","ai-codebuddyCliPath","ai-codexProxy","ai-hookPort","ai-logLevel","ai-useSdkMode"]);
|
|
10
11
|
const el = (id) => document.getElementById(id);
|
|
11
12
|
const storageKey = "open-im-web-lang";
|
|
@@ -25,6 +26,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
25
26
|
const setChecked = (id, value) => { const node = el(id); if (node) node.checked = Boolean(value); };
|
|
26
27
|
const setMessage = (text, type="") => { const node = el("message"); node.textContent = text; node.className = ("message " + type).trim(); };
|
|
27
28
|
const setBusy = (busy) => ["validateButton","saveButton","startButton","stopButton","langButton"].forEach((id) => { el(id).disabled = busy; });
|
|
29
|
+
const getActiveAiTool = () => el("ai-aiCommand").value || "claude";
|
|
28
30
|
const readPlatformConfig = (platform) => platform.testFields.reduce((config, field) => {
|
|
29
31
|
config[field] = getValue(platform.key + "-" + field);
|
|
30
32
|
return config;
|
|
@@ -54,6 +56,14 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
54
56
|
el("aiHint").textContent = t("aiHint");
|
|
55
57
|
el("aiHint").style.display = t("aiHint") ? "block" : "none";
|
|
56
58
|
el("claudeNote").textContent = t("claudeNote");
|
|
59
|
+
setText("aiCommonTitle", t("aiCommonTitle"));
|
|
60
|
+
setText("aiCommonHint", t("aiCommonHint"));
|
|
61
|
+
setText("aiToolConfigTitle", t("aiToolConfigTitle"));
|
|
62
|
+
setText("aiToolConfigHint", t("aiToolConfigHint"));
|
|
63
|
+
setText("ai-claudeSectionLabel", "Claude");
|
|
64
|
+
setText("ai-codexSectionLabel", "Codex");
|
|
65
|
+
setText("ai-cursorSectionLabel", "Cursor");
|
|
66
|
+
setText("ai-codebuddySectionLabel", "CodeBuddy");
|
|
57
67
|
setText("telegram-enabled-label", t("enabled"));
|
|
58
68
|
setText("feishu-enabled-label", t("enabled"));
|
|
59
69
|
setText("qq-enabled-label", t("enabled"));
|
|
@@ -123,6 +133,9 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
123
133
|
el("ai-claudeModel").placeholder = t("optional");
|
|
124
134
|
setText("ai-claudeSkipPermissions-label", t("autoApprove"));
|
|
125
135
|
setText("ai-useSdkMode-label", t("sdkMode"));
|
|
136
|
+
document.querySelectorAll("[data-tool]").forEach((button) => {
|
|
137
|
+
button.textContent = button.getAttribute("data-tool");
|
|
138
|
+
});
|
|
126
139
|
el("validateButton").textContent = t("validate");
|
|
127
140
|
el("saveButton").textContent = t("save");
|
|
128
141
|
el("startButton").textContent = t("start");
|
|
@@ -156,6 +169,20 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
156
169
|
el("navAiBtn").textContent = t("aiTitle");
|
|
157
170
|
el("navServiceBtn").textContent = t("serviceTitle");
|
|
158
171
|
}
|
|
172
|
+
function updateAiToolVisibility() {
|
|
173
|
+
const activeTool = getActiveAiTool();
|
|
174
|
+
aiTools.forEach((tool) => {
|
|
175
|
+
const panel = document.querySelector('[data-tool-panel="' + tool + '"]');
|
|
176
|
+
if (panel) {
|
|
177
|
+
panel.hidden = tool !== activeTool;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
document.querySelectorAll("[data-tool]").forEach((button) => {
|
|
181
|
+
button.classList.toggle("active", button.getAttribute("data-tool") === activeTool);
|
|
182
|
+
});
|
|
183
|
+
const toolLabels = { claude: "Claude", codex: "Codex", cursor: "Cursor", codebuddy: "CodeBuddy" };
|
|
184
|
+
el("aiConfigSummary").textContent = t("aiConfigSummary", { tool: toolLabels[activeTool] || activeTool });
|
|
185
|
+
}
|
|
159
186
|
function updateVisualState() {
|
|
160
187
|
const enabled = [];
|
|
161
188
|
platformDefinitions.forEach((platform) => {
|
|
@@ -167,6 +194,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
167
194
|
el("liveSummary").textContent = enabled.length
|
|
168
195
|
? t("summaryEnabled", { platforms: enabled.join(t("listSeparator")), tool: aiTool })
|
|
169
196
|
: t("summaryEmpty", { tool: aiTool });
|
|
197
|
+
updateAiToolVisibility();
|
|
170
198
|
}
|
|
171
199
|
async function updateDashboard() {
|
|
172
200
|
try {
|
|
@@ -328,6 +356,14 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
|
|
|
328
356
|
node.addEventListener("change", updateVisualState);
|
|
329
357
|
}
|
|
330
358
|
});
|
|
359
|
+
document.querySelectorAll("[data-tool]").forEach((button) => {
|
|
360
|
+
button.addEventListener("click", () => {
|
|
361
|
+
const tool = button.getAttribute("data-tool");
|
|
362
|
+
if (!tool) return;
|
|
363
|
+
setValue("ai-aiCommand", tool);
|
|
364
|
+
updateVisualState();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
331
367
|
}
|
|
332
368
|
async function validate() { setBusy(true); try { await request("/api/config/validate", { method: "POST", body: JSON.stringify(payload()) }); setMessage(t("validationOk"), "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
|
|
333
369
|
async function save() { setBusy(true); try { await request("/api/config/save?final=1", { method: "POST", body: JSON.stringify(payload()) }); setMessage(t("saveOk"), "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
|
|
@@ -47,6 +47,16 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
|
|
|
47
47
|
#configSection .panel,#aiSection .panel,#serviceSection{background:rgba(255,253,247,.82)}
|
|
48
48
|
#configSection .panel-head,#aiSection .panel-head,#serviceSection .panel-head{margin-bottom:14px}
|
|
49
49
|
#configSection .panel .summary,#aiSection .panel .summary{line-height:1.58}
|
|
50
|
+
.ai-layout{display:grid;gap:16px}
|
|
51
|
+
.ai-switcher{display:flex;flex-wrap:wrap;gap:10px}
|
|
52
|
+
.tool-switch{padding:10px 14px;background:rgba(19,35,26,.06);color:var(--ink);border:1px solid var(--line);box-shadow:none}
|
|
53
|
+
.tool-switch:hover{box-shadow:none}
|
|
54
|
+
.tool-switch.active{background:var(--green);border-color:var(--green);color:#fff7eb}
|
|
55
|
+
.ai-card{display:grid;gap:14px}
|
|
56
|
+
.ai-card[hidden]{display:none}
|
|
57
|
+
.ai-section-label{font-size:.75rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
|
|
58
|
+
.ai-summary-bar{padding:14px 16px;border:1px dashed var(--line-strong);background:rgba(255,255,255,.5);color:var(--muted)}
|
|
59
|
+
.toggle-grid{display:grid;gap:12px}
|
|
50
60
|
#serviceSection .actions{grid-template-columns:repeat(auto-fit,minmax(170px,1fr));align-items:stretch}
|
|
51
61
|
#startButton{background:var(--green);box-shadow:0 12px 22px rgba(26,106,68,.18)}
|
|
52
62
|
#saveButton{background:rgba(19,35,26,.88)}
|
|
@@ -206,28 +216,67 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
|
|
|
206
216
|
</section>
|
|
207
217
|
<section class="section" id="aiSection">
|
|
208
218
|
<div class="panel-head"><h2 id="aiTitle">AI Tooling</h2><div id="aiHint">WeChat is intentionally excluded from this first version.</div></div>
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
<
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
219
|
+
<div class="ai-layout">
|
|
220
|
+
<article class="panel note" id="claudeNote">Claude credentials are still read from environment variables or ~/.claude/settings.json. This page manages local bridge config, not Claude account auth.</article>
|
|
221
|
+
<article class="panel ai-card">
|
|
222
|
+
<div class="panel-head">
|
|
223
|
+
<h3 id="aiCommonTitle">Shared defaults</h3>
|
|
224
|
+
<div class="summary" id="aiCommonHint">Choose the default AI tool first. Tool-specific fields are shown below.</div>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="two-col">
|
|
227
|
+
<label><span id="ai-aiCommand-label">Default AI tool</span><select id="ai-aiCommand"><option value="claude">claude</option><option value="codex">codex</option><option value="cursor">cursor</option><option value="codebuddy">codebuddy</option></select></label>
|
|
228
|
+
<label><span id="ai-claudeWorkDir-label">Default work directory</span><input id="ai-claudeWorkDir" class="mono" /></label>
|
|
229
|
+
<label><span id="ai-hookPort-label">Hook port</span><input id="ai-hookPort" type="number" min="1" /></label>
|
|
230
|
+
<label><span id="ai-logLevel-label">Log level</span><select id="ai-logLevel"><option value="default">default</option><option value="DEBUG">DEBUG</option><option value="INFO">INFO</option><option value="WARN">WARN</option><option value="ERROR">ERROR</option></select></label>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="ai-summary-bar" id="aiConfigSummary"></div>
|
|
233
|
+
</article>
|
|
234
|
+
<article class="panel ai-card">
|
|
235
|
+
<div class="panel-head">
|
|
236
|
+
<h3 id="aiToolConfigTitle">Tool-specific settings</h3>
|
|
237
|
+
<div class="summary" id="aiToolConfigHint">Only the selected tool's fields are shown.</div>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="ai-switcher" id="aiToolSwitcher">
|
|
240
|
+
<button type="button" class="tool-switch" data-tool="claude">claude</button>
|
|
241
|
+
<button type="button" class="tool-switch" data-tool="codex">codex</button>
|
|
242
|
+
<button type="button" class="tool-switch" data-tool="cursor">cursor</button>
|
|
243
|
+
<button type="button" class="tool-switch" data-tool="codebuddy">codebuddy</button>
|
|
244
|
+
</div>
|
|
245
|
+
<section class="ai-card" id="ai-tool-claude" data-tool-panel="claude">
|
|
246
|
+
<div class="ai-section-label" id="ai-claudeSectionLabel">Claude</div>
|
|
247
|
+
<div class="two-col">
|
|
248
|
+
<label><span id="ai-claudeCliPath-label">Claude CLI path</span><input id="ai-claudeCliPath" class="mono" /></label>
|
|
249
|
+
<label><span id="ai-claudeTimeoutMs-label">Claude timeout (ms)</span><input id="ai-claudeTimeoutMs" type="number" min="1" /></label>
|
|
250
|
+
<label><span id="ai-claudeModel-label">Claude model</span><input id="ai-claudeModel" placeholder="Optional" /></label>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="toggle-grid">
|
|
253
|
+
<label class="toggle"><input id="ai-claudeSkipPermissions" type="checkbox" /> <span id="ai-claudeSkipPermissions-label">Auto-approve tool permissions</span></label>
|
|
254
|
+
<label class="toggle"><input id="ai-useSdkMode" type="checkbox" /> <span id="ai-useSdkMode-label">Use Claude SDK mode</span></label>
|
|
255
|
+
</div>
|
|
256
|
+
</section>
|
|
257
|
+
<section class="ai-card" id="ai-tool-codex" data-tool-panel="codex" hidden>
|
|
258
|
+
<div class="ai-section-label" id="ai-codexSectionLabel">Codex</div>
|
|
259
|
+
<div class="two-col">
|
|
260
|
+
<label><span id="ai-codexCliPath-label">Codex CLI path</span><input id="ai-codexCliPath" class="mono" /></label>
|
|
261
|
+
<label><span id="ai-codexTimeoutMs-label">Codex timeout (ms)</span><input id="ai-codexTimeoutMs" type="number" min="1" /></label>
|
|
262
|
+
<label><span id="ai-codexProxy-label">Codex proxy</span><input id="ai-codexProxy" class="mono" placeholder="Optional" /></label>
|
|
263
|
+
</div>
|
|
264
|
+
</section>
|
|
265
|
+
<section class="ai-card" id="ai-tool-cursor" data-tool-panel="cursor" hidden>
|
|
266
|
+
<div class="ai-section-label" id="ai-cursorSectionLabel">Cursor</div>
|
|
267
|
+
<div class="two-col">
|
|
268
|
+
<label><span id="ai-cursorCliPath-label">Cursor CLI path</span><input id="ai-cursorCliPath" class="mono" /></label>
|
|
269
|
+
</div>
|
|
270
|
+
</section>
|
|
271
|
+
<section class="ai-card" id="ai-tool-codebuddy" data-tool-panel="codebuddy" hidden>
|
|
272
|
+
<div class="ai-section-label" id="ai-codebuddySectionLabel">CodeBuddy</div>
|
|
273
|
+
<div class="two-col">
|
|
274
|
+
<label><span id="ai-codebuddyCliPath-label">CodeBuddy CLI path</span><input id="ai-codebuddyCliPath" class="mono" /></label>
|
|
275
|
+
<label><span id="ai-codebuddyTimeoutMs-label">CodeBuddy timeout (ms)</span><input id="ai-codebuddyTimeoutMs" type="number" min="1" /></label>
|
|
276
|
+
</div>
|
|
277
|
+
</section>
|
|
278
|
+
</article>
|
|
279
|
+
</div>
|
|
231
280
|
</section>
|
|
232
281
|
<section class="footer" id="serviceSection">
|
|
233
282
|
<div class="panel-head"><h2 id="serviceTitle">Service Control</h2><div id="serviceHint">Validate, save, start, and stop the local bridge from one place.</div></div>
|
|
@@ -30,4 +30,14 @@ describe("config web page assembly", () => {
|
|
|
30
30
|
expect(PAGE_HTML).toContain(`id="${id}"`);
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
|
+
it("keeps AI tool switch buttons and tool panels aligned", () => {
|
|
34
|
+
const toolListMatch = PAGE_SCRIPT.match(/const aiTools = \[([^\]]+)\]/);
|
|
35
|
+
expect(toolListMatch).toBeTruthy();
|
|
36
|
+
const tools = Array.from((toolListMatch?.[1] ?? "").matchAll(/"([^"]+)"/g), (match) => match[1]);
|
|
37
|
+
expect(tools).toEqual(["claude", "codex", "cursor", "codebuddy"]);
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
expect(PAGE_HTML).toContain(`data-tool="${tool}"`);
|
|
40
|
+
expect(PAGE_HTML).toContain(`data-tool-panel="${tool}"`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
33
43
|
});
|