mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
feat(custom-model): generate Run-menu entries from saved endpoint profiles
Follow-up to #393, picking up the work Ark0N invited in his merge comment: "generate those entries from the saved profiles rather than a fixed duplicate per harness, and put it in a follow-up PR so this one stays the backend... The Run-menu picker is yours if you want it." Adds the frontend surface the backend has been waiting on: - Run menu: a "Custom Endpoints" section lists one entry per (harness that supports customModelInjection, saved endpoint) pair, e.g. "Claude Code (llama.cpp)". The harness list comes from window.__codemanCustomModelClis, injected at page render straight off the CLI registry's own capabilities (never a hardcoded id list in the frontend), so a CLI whose injection recipe lands later appears with no frontend change. Picking an entry runs that harness's own existing run*() function unmodified (case creation, env overrides, everything, forced to a single instance) and then applies the endpoint's default model to the session it creates via the existing POST /api/sessions/:id/custom-model route. Entries are hidden for a remote/docker active case, since that route already refuses both. - Settings: App Settings -> Models gets a "Custom model endpoints" group wiring up the customModelEndpointsEnabled toggle (declared since #393, read by nothing until now) plus CRUD against the existing /api/model-endpoints routes: list, add/edit (inline form), delete, discover models. - Backend: CustomModelHost gains an optional defaultModelId, the model the picker applies with no further choice per endpoint (one generated menu entry per CLI+endpoint pair, not per CLI+endpoint+model). The route refuses a value that isn't one of the endpoint's own discovered models, and a fresh discovery drops a default that no longer appears rather than carrying an invalid one forward. Docs: docs/custom-model-endpoints.md describes the new picker and settings panel; CLAUDE.md's Custom Model Endpoint Profiles entry drops the "backend-only" status note and documents the picker's generation mechanism. Tests: four new route tests cover defaultModelId validation, acceptance, and the drop/keep behaviour across a re-discovery; a new render-index-html test pins the __codemanCustomModelClis injection (present, agent CLIs supporting the capability, antigravity and shell excluded) and its solo-window skip. No browser test was added for the Run-menu picker itself or the settings CRUD panel (this box has no tmux, so the live server used by test:browser/test:mobile could not be exercised here) -- worth a Playwright pass before merge, same as any other frontend PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqZeHrRS6DYcGcGX2p9EwG
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
bd286bf502
commit
25fae9ad10
@@ -461,6 +461,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (menu.classList.contains('active')) {
|
||||
this._loadRunModeHistory();
|
||||
this._refreshRunModeAvailability(menu);
|
||||
this._refreshCustomModelRunOptions(menu);
|
||||
const close = (ev) => {
|
||||
if (!menu.contains(ev.target)) {
|
||||
menu.classList.remove('active');
|
||||
@@ -534,6 +535,124 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (dsWeb) dsWeb.style.display = avail.deepseekBinary ? 'flex' : 'none';
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates the Run menu's Custom Model Endpoint entries
|
||||
* (docs/custom-model-endpoints-plan.md): one button per (capable harness, saved
|
||||
* endpoint) pair, e.g. "Claude Code (llama.cpp)". Hidden entirely when the
|
||||
* feature is off, no endpoint has a usable default model, or the active case is
|
||||
* remote/docker (the apply route refuses both — see session-routes.ts).
|
||||
*
|
||||
* `window.__codemanCustomModelClis` is server-injected at render time from the
|
||||
* CLI registry's own `capabilities.customModelInjection` (never a hardcoded id
|
||||
* list here), so a CLI gaining or losing the capability shows up with no
|
||||
* frontend change.
|
||||
*/
|
||||
async _refreshCustomModelRunOptions(menu) {
|
||||
const sep = menu.querySelector('#runModeCustomModelSep');
|
||||
const header = menu.querySelector('#runModeCustomModelHeader');
|
||||
const container = menu.querySelector('#runModeCustomModels');
|
||||
if (!container) return;
|
||||
const hide = () => {
|
||||
if (sep) sep.style.display = 'none';
|
||||
if (header) header.style.display = 'none';
|
||||
container.innerHTML = '';
|
||||
};
|
||||
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const capableClis = window.__codemanCustomModelClis || [];
|
||||
if (!settings.customModelEndpointsEnabled || capableClis.length === 0) return hide();
|
||||
|
||||
const caseName = document.getElementById('quickStartCase')?.value;
|
||||
const activeCase = caseName ? (this.cases || []).find((c) => c.name === caseName) : null;
|
||||
if (activeCase?.location === 'remote' || activeCase?.location === 'docker') return hide();
|
||||
|
||||
let hosts;
|
||||
try {
|
||||
const res = await fetch('/api/model-endpoints');
|
||||
hosts = await res.json();
|
||||
} catch {
|
||||
return hide();
|
||||
}
|
||||
if (!Array.isArray(hosts) || hosts.length === 0) return hide();
|
||||
|
||||
const rows = [];
|
||||
for (const host of hosts) {
|
||||
const modelId = host.defaultModelId || (host.models || [])[0];
|
||||
if (!modelId) continue; // nothing discovered yet — the settings panel explains why
|
||||
for (const cli of capableClis) {
|
||||
rows.push(`
|
||||
<button class="run-mode-option" data-mode="${escapeHtml(cli.id)}" data-endpoint="${escapeHtml(host.id)}"
|
||||
onclick="app.runCustomModelEntry(${JSON.stringify(cli.id)}, ${JSON.stringify(host.id)}, ${JSON.stringify(modelId)})"
|
||||
title="${escapeHtml(cli.label)} → ${escapeHtml(host.baseUrl)} (${escapeHtml(modelId)})">
|
||||
<span class="run-mode-dot ${escapeHtml(cli.id)}"></span>${escapeHtml(cli.label)} (${escapeHtml(host.label)})
|
||||
</button>`);
|
||||
}
|
||||
}
|
||||
if (rows.length === 0) return hide();
|
||||
if (sep) sep.style.display = '';
|
||||
if (header) header.style.display = '';
|
||||
container.innerHTML = rows.join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs a session on `mode` and immediately applies `endpointId`/`modelId` to it
|
||||
* via POST /api/sessions/:id/custom-model (see session-routes.ts) — the same
|
||||
* restart-in-place apply path the (not-yet-built) endpoint-management surface
|
||||
* would use for an already-running session. Reuses the existing per-mode run*()
|
||||
* functions wholesale (case creation, env overrides, the works) rather than a
|
||||
* parallel create path, forcing a single instance: a custom-model run is a
|
||||
* one-off "try this endpoint" action, not a batch spawn.
|
||||
*/
|
||||
async runCustomModelEntry(mode, endpointId, modelId) {
|
||||
document.getElementById('runModeMenu')?.classList.remove('active');
|
||||
const runners = {
|
||||
claude: () => this.runClaude(),
|
||||
opencode: () => this.runOpenCode(),
|
||||
codex: () => this.runCodex(),
|
||||
gemini: () => this.runGemini(),
|
||||
pi: () => this.runPi(),
|
||||
grok: () => this.runGrok(),
|
||||
deepseek: () => this.runDeepSeek(),
|
||||
omp: () => this.runOmp(),
|
||||
};
|
||||
const runner = runners[mode];
|
||||
if (!runner) {
|
||||
this.showToast(`No run function for mode ${mode}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const tabCountEl = document.getElementById('tabCount');
|
||||
const prevTabCount = tabCountEl?.value;
|
||||
if (tabCountEl) tabCountEl.value = '1';
|
||||
try {
|
||||
await runner();
|
||||
} finally {
|
||||
if (tabCountEl && prevTabCount !== undefined) tabCountEl.value = prevTabCount;
|
||||
}
|
||||
|
||||
// Every run*() ends by selecting the session it just created, so the active
|
||||
// session at this point IS the new one — see runClaude/runShell's own comments
|
||||
// on why selectSession must run before this reads activeSessionId.
|
||||
const sessionId = this.activeSessionId;
|
||||
if (!sessionId) return; // run() already reported its own error via toast
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/sessions/${sessionId}/custom-model`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ endpointId, modelId }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) {
|
||||
this.showToast(`Session started on the native backend — could not apply the custom endpoint: ${data.error}`, 'warning');
|
||||
return;
|
||||
}
|
||||
this.showToast(`Pointed at ${endpointId} — restarting the session...`, 'info');
|
||||
} catch (err) {
|
||||
this.showToast(`Session started, but applying the custom endpoint failed: ${err.message}`, 'warning');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start the DeepSeek Harness browser UI and open it as a Codeman web tab.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user