openclacky 1.3.1 → 1.3.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +65 -11
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +521 -13
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +512 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +60 -6
- data/lib/clacky/web/index.html +117 -57
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +121 -25
- data/lib/clacky/web/skills.js +3 -821
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -365
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -212
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// ── MCP · store — server catalog data + Agent-driven actions ───────────────
|
|
2
|
+
//
|
|
3
|
+
// MCP is a read-only, Agent-First panel. Configuration lives in mcp.json; this
|
|
4
|
+
// store fetches the catalog, probes servers for their tool list, toggles the
|
|
5
|
+
// enabled flag, and removes entries. It never renders.
|
|
6
|
+
//
|
|
7
|
+
// Holds catalog data, expand state, and a tools cache. Emits store events the
|
|
8
|
+
// view reacts to; mirrors them to the extension bus via Clacky.ext.emit.
|
|
9
|
+
//
|
|
10
|
+
// `Mcp` stays the single public facade.
|
|
11
|
+
//
|
|
12
|
+
// Depends on: Sessions, Clacky.ext.
|
|
13
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const McpStore = (() => {
|
|
16
|
+
let _data = null;
|
|
17
|
+
const _expanded = new Set();
|
|
18
|
+
const _toolsCache = new Map();
|
|
19
|
+
|
|
20
|
+
const _listeners = {};
|
|
21
|
+
|
|
22
|
+
function _on(event, handler) {
|
|
23
|
+
(_listeners[event] ||= []).push(handler);
|
|
24
|
+
return () => {
|
|
25
|
+
const list = _listeners[event];
|
|
26
|
+
const i = list ? list.indexOf(handler) : -1;
|
|
27
|
+
if (i >= 0) list.splice(i, 1);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function _emit(event, payload) {
|
|
32
|
+
(_listeners[event] || []).forEach((h) => h(payload));
|
|
33
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const state = {
|
|
37
|
+
get data() { return _data; },
|
|
38
|
+
get expanded() { return _expanded; },
|
|
39
|
+
isExpanded(name) { return _expanded.has(name); },
|
|
40
|
+
cachedTools(name) { return _toolsCache.get(name); },
|
|
41
|
+
hasCachedTools(name) { return _toolsCache.has(name); },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async function _sendToAgent(command, sessionName) {
|
|
45
|
+
try {
|
|
46
|
+
const maxN = Sessions.all.reduce((max, s) => {
|
|
47
|
+
const m = s.name.match(/^Session (\d+)$/);
|
|
48
|
+
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
49
|
+
}, 0);
|
|
50
|
+
const name = sessionName || ("Session " + (maxN + 1));
|
|
51
|
+
|
|
52
|
+
const res = await fetch("/api/sessions", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ name, source: "mcp" }),
|
|
56
|
+
});
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
if (!res.ok) throw new Error(data.error || "failed to create session");
|
|
59
|
+
const session = data.session;
|
|
60
|
+
if (!session) throw new Error("no session returned");
|
|
61
|
+
|
|
62
|
+
Sessions.add(session);
|
|
63
|
+
Sessions.renderList();
|
|
64
|
+
Sessions.setPendingMessage(session.id, command);
|
|
65
|
+
Sessions.select(session.id);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
alert("Error: " + e.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const Mcp = {
|
|
72
|
+
on: _on,
|
|
73
|
+
state,
|
|
74
|
+
|
|
75
|
+
async load() {
|
|
76
|
+
_emit("mcp:loading");
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch("/api/mcp");
|
|
79
|
+
_data = await res.json();
|
|
80
|
+
_emit("mcp:changed", { data: _data });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
_emit("mcp:error", { message: e.message });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/** Fetch a server's tool catalog (cached). Returns { ok, tools, error }. */
|
|
87
|
+
async probe(name) {
|
|
88
|
+
if (_toolsCache.has(name)) return { ok: true, tools: _toolsCache.get(name) };
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(`/api/mcp/${encodeURIComponent(name)}/probe`, { method: "POST" });
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
if (!res.ok || !data.ok) return { ok: false, error: data.error || "unknown" };
|
|
93
|
+
const tools = data.tools || [];
|
|
94
|
+
_toolsCache.set(name, tools);
|
|
95
|
+
return { ok: true, tools };
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { ok: false, error: e.message };
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
toggleExpand(name) {
|
|
102
|
+
if (_expanded.has(name)) _expanded.delete(name);
|
|
103
|
+
else _expanded.add(name);
|
|
104
|
+
_emit("mcp:changed", { data: _data });
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async toggle(name, enabled) {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(`/api/mcp/${encodeURIComponent(name)}/enabled`, {
|
|
110
|
+
method: "PATCH",
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify({ enabled }),
|
|
113
|
+
});
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
if (!res.ok || !data.ok) {
|
|
116
|
+
_emit("mcp:actionError", { kind: "toggle", message: data.error || `HTTP ${res.status}` });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!enabled) {
|
|
120
|
+
_toolsCache.delete(name);
|
|
121
|
+
_expanded.delete(name);
|
|
122
|
+
}
|
|
123
|
+
await Mcp.load();
|
|
124
|
+
} catch (e) {
|
|
125
|
+
_emit("mcp:actionError", { kind: "toggle", message: e.message });
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async remove(name) {
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(`/api/mcp/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
if (!res.ok || !data.ok) {
|
|
134
|
+
_emit("mcp:actionError", { kind: "remove", message: data.error || `HTTP ${res.status}` });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
_toolsCache.delete(name);
|
|
138
|
+
_expanded.delete(name);
|
|
139
|
+
await Mcp.load();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
_emit("mcp:actionError", { kind: "remove", message: e.message });
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
resetCaches() {
|
|
146
|
+
_toolsCache.clear();
|
|
147
|
+
_expanded.clear();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
askAdd() { return _sendToAgent("/mcp-manager add", "MCP Setup"); },
|
|
151
|
+
askFix(name) { return _sendToAgent(`/mcp-manager reconfigure ${name}`, `MCP Fix — ${name}`); },
|
|
152
|
+
sendToAgent: _sendToAgent,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return Mcp;
|
|
156
|
+
})();
|
|
157
|
+
|
|
158
|
+
const Mcp = McpStore;
|
|
@@ -1,48 +1,38 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ── MCP · view — rendering + DOM wiring for the MCP servers panel ──────────
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Owns all card/status/tools rendering and event wiring. Reads through
|
|
4
|
+
// McpStore.state and reacts to store events. Probe / toggle / remove go through
|
|
5
|
+
// store actions; confirm dialogs and error alerts (UI concerns) live here.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let _data = null;
|
|
13
|
-
const _expanded = new Set();
|
|
14
|
-
const _toolsCache = new Map();
|
|
7
|
+
// Augments the `Mcp` facade with onPanelShow.
|
|
8
|
+
//
|
|
9
|
+
// Depends on: McpStore, I18n, global $ helper.
|
|
10
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
await _load();
|
|
18
|
-
}
|
|
12
|
+
const McpView = (() => {
|
|
19
13
|
|
|
20
|
-
|
|
14
|
+
function _renderLoading() {
|
|
21
15
|
const list = $("mcp-list");
|
|
22
16
|
const status = $("mcp-status");
|
|
23
|
-
if (!list) return;
|
|
24
|
-
list.innerHTML = `<div class="channel-loading">${I18n.t("mcp.loading")}</div>`;
|
|
25
17
|
if (status) status.innerHTML = "";
|
|
18
|
+
if (list) list.innerHTML = `<div class="channel-loading">${I18n.t("mcp.loading")}</div>`;
|
|
19
|
+
}
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
_data = data;
|
|
31
|
-
_render();
|
|
32
|
-
} catch (e) {
|
|
33
|
-
list.innerHTML = `<div class="channel-error">${I18n.t("mcp.loadError", { msg: _esc(e.message) })}</div>`;
|
|
34
|
-
}
|
|
21
|
+
function _renderError(payload) {
|
|
22
|
+
const list = $("mcp-list");
|
|
23
|
+
if (list) list.innerHTML = `<div class="channel-error">${I18n.t("mcp.loadError", { msg: _esc(payload.message) })}</div>`;
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
function _render() {
|
|
38
27
|
const list = $("mcp-list");
|
|
39
28
|
const status = $("mcp-status");
|
|
40
|
-
|
|
29
|
+
const data = McpStore.state.data;
|
|
30
|
+
if (!list || !data) return;
|
|
41
31
|
|
|
42
32
|
if (status) {
|
|
43
|
-
const pathLabel =
|
|
44
|
-
? _esc(
|
|
45
|
-
: `${_esc(
|
|
33
|
+
const pathLabel = data.config_exists
|
|
34
|
+
? _esc(data.config_path)
|
|
35
|
+
: `${_esc(data.config_path)} <em>${I18n.t("mcp.config.missing")}</em>`;
|
|
46
36
|
status.innerHTML = `
|
|
47
37
|
<div class="mcp-cta">
|
|
48
38
|
<div class="mcp-cta-text">
|
|
@@ -70,16 +60,15 @@ const Mcp = (() => {
|
|
|
70
60
|
</div>
|
|
71
61
|
`;
|
|
72
62
|
$("btn-mcp-refresh")?.addEventListener("click", () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
_load();
|
|
63
|
+
Mcp.resetCaches();
|
|
64
|
+
Mcp.load();
|
|
76
65
|
});
|
|
77
|
-
$("btn-mcp-cta")?.addEventListener("click", () =>
|
|
66
|
+
$("btn-mcp-cta")?.addEventListener("click", () => Mcp.askAdd());
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
list.innerHTML = "";
|
|
81
70
|
|
|
82
|
-
if (!
|
|
71
|
+
if (!data.configured || !data.servers || data.servers.length === 0) {
|
|
83
72
|
list.innerHTML = `
|
|
84
73
|
<div class="mcp-empty">
|
|
85
74
|
<h3>${I18n.t("mcp.empty.title")}</h3>
|
|
@@ -89,11 +78,11 @@ const Mcp = (() => {
|
|
|
89
78
|
</button>
|
|
90
79
|
</div>
|
|
91
80
|
`;
|
|
92
|
-
$("btn-mcp-empty-cta")?.addEventListener("click", () =>
|
|
81
|
+
$("btn-mcp-empty-cta")?.addEventListener("click", () => Mcp.askAdd());
|
|
93
82
|
return;
|
|
94
83
|
}
|
|
95
84
|
|
|
96
|
-
|
|
85
|
+
data.servers.forEach(server => {
|
|
97
86
|
list.appendChild(_renderCard(server));
|
|
98
87
|
});
|
|
99
88
|
}
|
|
@@ -109,7 +98,7 @@ const Mcp = (() => {
|
|
|
109
98
|
? (server.url || "")
|
|
110
99
|
: [server.command, ...(server.args || [])].filter(Boolean).join(" ");
|
|
111
100
|
const cmdLabel = isHttp ? I18n.t("mcp.url") : I18n.t("mcp.command");
|
|
112
|
-
const isExpanded =
|
|
101
|
+
const isExpanded = McpStore.state.isExpanded(server.name);
|
|
113
102
|
const toggleAria = server.disabled
|
|
114
103
|
? I18n.t("mcp.toggle.off")
|
|
115
104
|
: I18n.t("mcp.toggle.on");
|
|
@@ -174,7 +163,7 @@ const Mcp = (() => {
|
|
|
174
163
|
card.querySelector(`#btn-mcp-probe-${CSS.escape(server.name)}`)
|
|
175
164
|
?.addEventListener("click", () => {
|
|
176
165
|
if (server.disabled) return;
|
|
177
|
-
|
|
166
|
+
Mcp.toggleExpand(server.name);
|
|
178
167
|
});
|
|
179
168
|
card.querySelector(`#btn-mcp-remove-${CSS.escape(server.name)}`)
|
|
180
169
|
?.addEventListener("click", () => _remove(server.name));
|
|
@@ -186,39 +175,23 @@ const Mcp = (() => {
|
|
|
186
175
|
return card;
|
|
187
176
|
}
|
|
188
177
|
|
|
189
|
-
async function _toggleProbe(name) {
|
|
190
|
-
if (_expanded.has(name)) {
|
|
191
|
-
_expanded.delete(name);
|
|
192
|
-
} else {
|
|
193
|
-
_expanded.add(name);
|
|
194
|
-
}
|
|
195
|
-
_render();
|
|
196
|
-
if (_expanded.has(name)) await _renderTools(name);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
178
|
async function _renderTools(name) {
|
|
200
179
|
const region = document.getElementById(`mcp-tools-${name}`);
|
|
201
180
|
if (!region) return;
|
|
202
181
|
|
|
203
|
-
if (
|
|
204
|
-
region.innerHTML = _toolsHtml(
|
|
182
|
+
if (McpStore.state.hasCachedTools(name)) {
|
|
183
|
+
region.innerHTML = _toolsHtml(McpStore.state.cachedTools(name));
|
|
205
184
|
return;
|
|
206
185
|
}
|
|
207
186
|
|
|
208
187
|
region.innerHTML = `<div class="mcp-tools-loading">${I18n.t("mcp.toolsLoading")}</div>`;
|
|
209
188
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
region.innerHTML = `<div class="mcp-tools-error">${I18n.t("mcp.toolsLoadError", { msg: _esc(data.error || "unknown") })}</div>`;
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
_toolsCache.set(name, data.tools || []);
|
|
218
|
-
region.innerHTML = _toolsHtml(data.tools || []);
|
|
219
|
-
} catch (e) {
|
|
220
|
-
region.innerHTML = `<div class="mcp-tools-error">${I18n.t("mcp.toolsLoadError", { msg: _esc(e.message) })}</div>`;
|
|
189
|
+
const result = await Mcp.probe(name);
|
|
190
|
+
if (!result.ok) {
|
|
191
|
+
region.innerHTML = `<div class="mcp-tools-error">${I18n.t("mcp.toolsLoadError", { msg: _esc(result.error) })}</div>`;
|
|
192
|
+
return;
|
|
221
193
|
}
|
|
194
|
+
region.innerHTML = _toolsHtml(result.tools);
|
|
222
195
|
}
|
|
223
196
|
|
|
224
197
|
function _toolsHtml(tools) {
|
|
@@ -237,80 +210,14 @@ const Mcp = (() => {
|
|
|
237
210
|
`;
|
|
238
211
|
}
|
|
239
212
|
|
|
240
|
-
function _askClackyAdd() {
|
|
241
|
-
_sendToAgent("/mcp-manager add", "MCP Setup");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function _askClackyFix(name) {
|
|
245
|
-
_sendToAgent(`/mcp-manager reconfigure ${name}`, `MCP Fix — ${name}`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async function _sendToAgent(command, sessionName) {
|
|
249
|
-
try {
|
|
250
|
-
const maxN = Sessions.all.reduce((max, s) => {
|
|
251
|
-
const m = s.name.match(/^Session (\d+)$/);
|
|
252
|
-
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
253
|
-
}, 0);
|
|
254
|
-
const name = sessionName || ("Session " + (maxN + 1));
|
|
255
|
-
|
|
256
|
-
const res = await fetch("/api/sessions", {
|
|
257
|
-
method: "POST",
|
|
258
|
-
headers: { "Content-Type": "application/json" },
|
|
259
|
-
body: JSON.stringify({ name, source: "mcp" }),
|
|
260
|
-
});
|
|
261
|
-
const data = await res.json();
|
|
262
|
-
if (!res.ok) throw new Error(data.error || "failed to create session");
|
|
263
|
-
const session = data.session;
|
|
264
|
-
if (!session) throw new Error("no session returned");
|
|
265
|
-
|
|
266
|
-
Sessions.add(session);
|
|
267
|
-
Sessions.renderList();
|
|
268
|
-
Sessions.setPendingMessage(session.id, command);
|
|
269
|
-
Sessions.select(session.id);
|
|
270
|
-
} catch (e) {
|
|
271
|
-
alert("Error: " + e.message);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
213
|
async function _toggle(name, enabled) {
|
|
276
|
-
|
|
277
|
-
const res = await fetch(`/api/mcp/${encodeURIComponent(name)}/enabled`, {
|
|
278
|
-
method: "PATCH",
|
|
279
|
-
headers: { "Content-Type": "application/json" },
|
|
280
|
-
body: JSON.stringify({ enabled }),
|
|
281
|
-
});
|
|
282
|
-
const data = await res.json();
|
|
283
|
-
if (!res.ok || !data.ok) {
|
|
284
|
-
alert(I18n.t("mcp.toggle.error", { msg: data.error || `HTTP ${res.status}` }));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (!enabled) {
|
|
288
|
-
_toolsCache.delete(name);
|
|
289
|
-
_expanded.delete(name);
|
|
290
|
-
}
|
|
291
|
-
await _load();
|
|
292
|
-
} catch (e) {
|
|
293
|
-
alert(I18n.t("mcp.toggle.error", { msg: e.message }));
|
|
294
|
-
}
|
|
214
|
+
await Mcp.toggle(name, enabled);
|
|
295
215
|
}
|
|
296
216
|
|
|
297
|
-
|
|
217
|
+
function _remove(name) {
|
|
298
218
|
const msg = I18n.t("mcp.remove.confirm", { name });
|
|
299
219
|
if (!window.confirm(msg)) return;
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
const res = await fetch(`/api/mcp/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
303
|
-
const data = await res.json();
|
|
304
|
-
if (!res.ok || !data.ok) {
|
|
305
|
-
alert(I18n.t("mcp.remove.error", { msg: data.error || `HTTP ${res.status}` }));
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
_toolsCache.delete(name);
|
|
309
|
-
_expanded.delete(name);
|
|
310
|
-
await _load();
|
|
311
|
-
} catch (e) {
|
|
312
|
-
alert(I18n.t("mcp.remove.error", { msg: e.message }));
|
|
313
|
-
}
|
|
220
|
+
Mcp.remove(name);
|
|
314
221
|
}
|
|
315
222
|
|
|
316
223
|
function _esc(str) {
|
|
@@ -321,8 +228,24 @@ const Mcp = (() => {
|
|
|
321
228
|
.replace(/"/g, """);
|
|
322
229
|
}
|
|
323
230
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
231
|
+
function _onActionError(payload) {
|
|
232
|
+
const key = payload.kind === "remove" ? "mcp.remove.error" : "mcp.toggle.error";
|
|
233
|
+
alert(I18n.t(key, { msg: payload.message }));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function _subscribe() {
|
|
237
|
+
Mcp.on("mcp:loading", _renderLoading);
|
|
238
|
+
Mcp.on("mcp:changed", _render);
|
|
239
|
+
Mcp.on("mcp:error", _renderError);
|
|
240
|
+
Mcp.on("mcp:actionError", _onActionError);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const viewApi = {
|
|
244
|
+
onPanelShow() { return Mcp.load(); },
|
|
327
245
|
};
|
|
246
|
+
|
|
247
|
+
return { init: _subscribe, api: viewApi };
|
|
328
248
|
})();
|
|
249
|
+
|
|
250
|
+
Object.assign(Mcp, McpView.api);
|
|
251
|
+
McpView.init();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// ── ModelTester · store — model connection test + save (shared helper) ────
|
|
2
|
+
//
|
|
3
|
+
// Network helpers shared by the onboarding wizard and the settings model modal:
|
|
4
|
+
// test a model connection and persist a model config. No own panel, no state to
|
|
5
|
+
// hold — it mirrors test/save outcomes onto the extension bus so extensions can
|
|
6
|
+
// observe model-config changes.
|
|
7
|
+
//
|
|
8
|
+
// `ModelTester` stays the single public facade.
|
|
9
|
+
//
|
|
10
|
+
// Depends on: I18n, Clacky.ext.
|
|
11
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
window.ModelTester = (function () {
|
|
14
|
+
function _emit(event, payload) {
|
|
15
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function testConnection({ model, base_url, api_key, anthropic_format, index, id } = {}) {
|
|
19
|
+
const body = { model, base_url, api_key };
|
|
20
|
+
if (typeof id === "string" && id) body.id = id;
|
|
21
|
+
if (typeof index === "number") body.index = index;
|
|
22
|
+
if (anthropic_format) body.anthropic_format = true;
|
|
23
|
+
|
|
24
|
+
let data;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch("/api/config/test", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify(body)
|
|
30
|
+
});
|
|
31
|
+
data = await res.json();
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { ok: false, message: e.message };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let result;
|
|
37
|
+
if (!data.ok) {
|
|
38
|
+
const msg = data.message || "";
|
|
39
|
+
const code = data.error_code || "";
|
|
40
|
+
result = code === "insufficient_credit"
|
|
41
|
+
? { ok: false, message: I18n.t("error.insufficient_credit"), error_code: code }
|
|
42
|
+
: { ok: false, message: msg, error_code: code };
|
|
43
|
+
} else if (data.effective_base_url && data.effective_base_url !== base_url) {
|
|
44
|
+
result = { ok: true, base_url: data.effective_base_url, message: data.message || "", rewrote: true };
|
|
45
|
+
} else {
|
|
46
|
+
result = { ok: true, base_url, message: data.message || "" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_emit("modeltester:tested", { model, ok: result.ok });
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function saveModel(payload, { existingId } = {}) {
|
|
54
|
+
const url = existingId
|
|
55
|
+
? `/api/config/models/${encodeURIComponent(existingId)}`
|
|
56
|
+
: "/api/config/models";
|
|
57
|
+
const method = existingId ? "PATCH" : "POST";
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
method,
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(payload)
|
|
64
|
+
});
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
const result = data.ok ? { ok: true } : { ok: false, error: data.error || "" };
|
|
67
|
+
_emit("modeltester:saved", { existingId: existingId || null, ok: result.ok });
|
|
68
|
+
return result;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return { ok: false, error: e.message };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { testConnection, saveModel };
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
const ModelTester = window.ModelTester;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ── ModelTester · view — render-free feature ──────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// ModelTester is a shared network helper with no panel of its own: the
|
|
4
|
+
// onboarding wizard and the settings model modal own the UI and call
|
|
5
|
+
// ModelTester.testConnection / saveModel directly. There is nothing to render
|
|
6
|
+
// here. This file exists to satisfy the store/view layering convention.
|
|
7
|
+
// ───────────────────────────────────────────────────────────────────────────
|