mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(custom-model): show a status toast during Claude's native-boot-then-restart window
Claude stays on the launch-then-restart path (see runCustomModelEntry's own comment for why), but with nothing on screen during that window, a native boot that briefly talks to the cloud model read as "the endpoint didn't apply" rather than "the switch hasn't happened yet". A sticky "Claude started - switching to <endpoint>..." toast now covers the whole window from the native launch through the apply call, updated in place (never stacked) as the outcome resolves: dismissed on cancel or failure (replaced by the existing cancellation/error toast), handed off to _watchLlamaSwapLoading's own sticky toast when a model swap is in progress, or updated to the existing "Pointed at ... - restarting" message and auto-dismissed after 3s on a plain success. 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
f865f74a0f
commit
83033b4299
@@ -810,6 +810,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
const sessionId = this.activeSessionId;
|
||||
if (!sessionId || sessionId === before) return;
|
||||
|
||||
// Claude just launched on the NATIVE backend and is about to be restarted onto
|
||||
// the endpoint — without something saying so, that native boot (which can talk
|
||||
// to Opus for a moment) reads as "the endpoint didn't apply" rather than "the
|
||||
// switch hasn't happened yet". Sticky until the apply below settles one way or
|
||||
// the other, or hands off to _watchLlamaSwapLoading's own sticky toast.
|
||||
const switchingToast = this.showToast(`Claude started — switching to ${endpointId}…`, 'info', { duration: 0 });
|
||||
|
||||
// A freshly launched CLI reports its OWN startup as 'busy' (spinner, the
|
||||
// workspace-trust check, whatever else it does before its first prompt) —
|
||||
// measured landing well before this line reliably reaches it — and the
|
||||
@@ -849,6 +856,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
`for ${payload.affectedSessions.length === 1 ? 'that session' : 'those sessions'} too. Continue?`
|
||||
);
|
||||
if (!proceed) {
|
||||
switchingToast?.dismiss();
|
||||
this.showToast('Kept the native backend — model switch cancelled', 'info');
|
||||
return;
|
||||
}
|
||||
@@ -857,20 +865,26 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
if (!ok || !data || data.success === false) {
|
||||
switchingToast?.dismiss();
|
||||
const detail = data?.error ? `: ${data.error}` : res ? ` (HTTP ${res.status})` : ' (request failed)';
|
||||
this.showToast(`Session started on the native backend — could not apply the custom endpoint${detail}`, 'error');
|
||||
return;
|
||||
}
|
||||
this.showToast(`Pointed at ${endpointId} — restarting the session...`, 'info');
|
||||
|
||||
// The apply above already succeeded — the session IS pointed at the endpoint — but
|
||||
// llama-swap itself may still be unloading the old model and loading this one, which
|
||||
// can take well over a minute. Without this, a prompt sent during that window either
|
||||
// hangs silently or (the bug this whole feature exists to fix) gets answered by
|
||||
// whatever was loaded a moment ago, reading as "it's still using the wrong model."
|
||||
// Hand off to its own sticky toast rather than stacking a second one on top.
|
||||
if (payload?.modelSwapInProgress) {
|
||||
switchingToast?.dismiss();
|
||||
void this._watchLlamaSwapLoading(endpointId, modelId);
|
||||
return;
|
||||
}
|
||||
|
||||
switchingToast?.setMessage(`Pointed at ${endpointId} — restarting the session...`);
|
||||
setTimeout(() => switchingToast?.dismiss(), 3000);
|
||||
},
|
||||
|
||||
/** POST /api/sessions/:id/custom-model, returning {ok, data, res} rather than throwing —
|
||||
|
||||
@@ -398,6 +398,73 @@ describe('Custom Model Endpoint Profiles: applying a picked entry', () => {
|
||||
expect(toastType).toBe('error');
|
||||
});
|
||||
|
||||
it('shows a status toast for the native-boot-then-restart window, so it never reads as the endpoint failing to apply', async () => {
|
||||
// Claude still goes through this two-step launch (see runCustomModelEntry's own
|
||||
// comment for why) — without something saying so, the native boot it starts with
|
||||
// (which can genuinely talk to the cloud model for a moment) reads as "the
|
||||
// endpoint didn't apply" rather than "the switch hasn't happened yet".
|
||||
const { app } = bootApp({});
|
||||
app.activeSessionId = 'old-session';
|
||||
app.run = async () => {
|
||||
app.activeSessionId = 'new-session';
|
||||
};
|
||||
app._api = async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ success: true, data: { customModel: { endpointId: 'llama-box' }, restarted: true } }),
|
||||
});
|
||||
const toasts: Array<{ message: string; dismissed: boolean }> = [];
|
||||
const messageHistory: string[] = [];
|
||||
app.showToast = (message: string) => {
|
||||
const entry = { message, dismissed: false };
|
||||
toasts.push(entry);
|
||||
messageHistory.push(message);
|
||||
return {
|
||||
dismiss: () => {
|
||||
entry.dismissed = true;
|
||||
},
|
||||
setMessage: (next: string) => {
|
||||
entry.message = next;
|
||||
messageHistory.push(next);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
await app.runCustomModelEntry('claude', 'llama-box', 'qwen3');
|
||||
|
||||
expect(toasts).toHaveLength(1); // updated in place, not stacked with a second toast
|
||||
expect(messageHistory[0]).toContain('Claude started — switching to llama-box');
|
||||
expect(messageHistory.at(-1)).toContain('Pointed at llama-box — restarting');
|
||||
});
|
||||
|
||||
it('dismisses the status toast on a failed apply rather than leaving it stuck on "switching"', async () => {
|
||||
const { app } = bootApp({});
|
||||
app.activeSessionId = 'old-session';
|
||||
app.run = async () => {
|
||||
app.activeSessionId = 'new-session';
|
||||
};
|
||||
app._api = async () => ({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({ success: false, error: 'boom' }),
|
||||
});
|
||||
const toasts: Array<{ message: string; dismissed: boolean }> = [];
|
||||
app.showToast = (message: string) => {
|
||||
const entry = { message, dismissed: false };
|
||||
toasts.push(entry);
|
||||
return {
|
||||
dismiss: () => {
|
||||
entry.dismissed = true;
|
||||
},
|
||||
setMessage: () => {},
|
||||
};
|
||||
};
|
||||
|
||||
await app.runCustomModelEntry('claude', 'llama-box', 'qwen3');
|
||||
|
||||
expect(toasts[0].dismissed).toBe(true); // the "switching..." toast, cleaned up
|
||||
expect(toasts.at(-1)?.message).toContain('boom'); // the error toast, separate from it
|
||||
});
|
||||
|
||||
it('routes through run() itself, so the Run in-flight lock actually engages', async () => {
|
||||
// CLAUDE.md, Run launch synchronization: the lock exists so a double click
|
||||
// cannot create duplicate sessions. A hardcoded dispatch table bypassing
|
||||
|
||||
Reference in New Issue
Block a user