diff --git a/src/web/public/index.html b/src/web/public/index.html
index b962b55d..9d2e0dc7 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -927,6 +927,27 @@
+
+
diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js
index ccb97887..ee0cd68e 100644
--- a/src/web/public/session-ui.js
+++ b/src/web/public/session-ui.js
@@ -667,6 +667,32 @@ Object.assign(CodemanApp.prototype, {
this._pendingCustomModelPick = null;
},
+ /**
+ * In-app replacement for a native `confirm()` popup, used specifically for the
+ * llama-swap "this will unload it for session X" warning (both launch paths below) —
+ * a browser-chrome dialog there looked out of place next to the rest of the app's own
+ * modals. Resolves true/false the same way `confirm()` would; `_resolveModelSwapConfirm`
+ * (the modal's own Cancel/Switch-anyway buttons, and its backdrop click) is what settles
+ * the returned promise.
+ */
+ _confirmModelSwap(message) {
+ const modal = document.getElementById('customModelSwapConfirmModal');
+ const messageEl = document.getElementById('customModelSwapConfirmMessage');
+ if (messageEl) messageEl.textContent = message;
+ modal?.classList.add('active');
+ return new Promise((resolve) => {
+ this._resolveModelSwapConfirmPromise = resolve;
+ });
+ },
+
+ /** Called by the modal's Cancel/Switch-anyway buttons and its backdrop click. */
+ _resolveModelSwapConfirm(proceed) {
+ document.getElementById('customModelSwapConfirmModal')?.classList.remove('active');
+ const resolve = this._resolveModelSwapConfirmPromise;
+ this._resolveModelSwapConfirmPromise = null;
+ resolve?.(proceed);
+ },
+
/** A model row in the picker modal was clicked: close it and launch with that choice. */
chooseCustomModelAndRun(modelId) {
const pending = this._pendingCustomModelPick;
@@ -766,7 +792,7 @@ Object.assign(CodemanApp.prototype, {
if (data?.data?.requiresConfirmation) {
const { currentlyLoadedModel, affectedSessions } = data.data;
const names = affectedSessions.map((s) => s.name || s.id).join(', ');
- const proceed = confirm(
+ const proceed = await this._confirmModelSwap(
`${names} ${affectedSessions.length === 1 ? 'is' : 'are'} currently using ` +
`${currentlyLoadedModel} on this endpoint. Switching will unload it for ` +
`${affectedSessions.length === 1 ? 'that session' : 'those sessions'} too. Continue?`
@@ -850,7 +876,7 @@ Object.assign(CodemanApp.prototype, {
// call with `confirmed: true` so the route skips the check the second time.
if (ok && payload?.requiresConfirmation) {
const names = payload.affectedSessions.map((s) => s.name || s.id).join(', ');
- const proceed = confirm(
+ const proceed = await this._confirmModelSwap(
`${names} ${payload.affectedSessions.length === 1 ? 'is' : 'are'} currently using ` +
`${payload.currentlyLoadedModel} on this endpoint. Switching to ${modelId} will unload it ` +
`for ${payload.affectedSessions.length === 1 ? 'that session' : 'those sessions'} too. Continue?`
diff --git a/src/web/public/styles.css b/src/web/public/styles.css
index f4f88587..cefc4d6b 100644
--- a/src/web/public/styles.css
+++ b/src/web/public/styles.css
@@ -6825,6 +6825,17 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
min-height: 0;
}
+/* Custom Model Endpoint Profiles: llama-swap model-swap confirmation — replaces a native
+ confirm() popup (docs/custom-model-endpoints-plan.md) so it looks and feels like the
+ rest of the app instead of a browser chrome dialog. */
+#customModelSwapConfirmModal .modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border-top: 1px solid var(--border-color);
+}
+
/* Mobile Case Picker - Base Styles */
.mobile-case-picker-sheet {
diff --git a/test/custom-model-one-shot-launch.test.ts b/test/custom-model-one-shot-launch.test.ts
index f5f3903c..d965606f 100644
--- a/test/custom-model-one-shot-launch.test.ts
+++ b/test/custom-model-one-shot-launch.test.ts
@@ -156,7 +156,7 @@ describe('_quickStartWithCustomModelConfirm', () => {
it('confirming re-sends with confirmed:true and returns the second response', async () => {
const { win, app } = bootApp();
- win.confirm = (() => true) as typeof win.confirm;
+ app._confirmModelSwap = async () => true;
let calls = 0;
withFetch(win, (body) => {
calls += 1;
@@ -184,7 +184,7 @@ describe('_quickStartWithCustomModelConfirm', () => {
it('cancelling never re-sends, and reports a cancellation error', async () => {
const { win, app } = bootApp();
- win.confirm = (() => false) as typeof win.confirm;
+ app._confirmModelSwap = async () => false;
let calls = 0;
withFetch(win, () => {
calls += 1;
diff --git a/test/custom-model-run-menu-ui.test.ts b/test/custom-model-run-menu-ui.test.ts
index fedce7fe..35e92ea4 100644
--- a/test/custom-model-run-menu-ui.test.ts
+++ b/test/custom-model-run-menu-ui.test.ts
@@ -54,6 +54,9 @@ function bootApp(
+