fix: call bind (not set_active) when switching to a fresh session

Bug: 400 from PUT /opencode-bridge/sessions/active when the user picked
a session from the global list that wasn't yet bound to this notebook.
The cell-action onSwitchSession callback was:
  1. PUT /sessions/active (server checks bound -> 400)
  2. PUT /sessions/notebook (bind, but never reached)
The order was wrong: set_active requires the session to be bound FIRST.

Fix: the PUT /sessions/notebook route does both bind AND set active
in a single call (server handler: bind_existing() then response
includes the new activeSessionId). So the callback now only needs to
call bind-existing. The /sessions/active endpoint is still useful for
future "switch between already-bound sessions" flows but the current
"pick from global list" path doesn't need it.

Tests: updated the switching test to assert bind-existing is called
and set-active is NOT. Also added not.toHaveBeenCalled() to lock in
the new behavior. 76 jest, 73 pytest, build green.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-28 16:39:27 +08:00
co-authored by Claude
parent d2bd4f1e48
commit 893f049eb7
2 changed files with 11 additions and 14 deletions
+5 -4
View File
@@ -1123,9 +1123,6 @@ describe('OpenCodeInlinePrompt', () => {
{ id: 'sess-B', title: 'second' },
]
});
mockedSetActiveSession.mockResolvedValue({
ok: true, notebookPath: 'foo.ipynb', activeSessionId: 'sess-B',
});
mockedBindExistingSession.mockResolvedValue({
ok: true, notebookPath: 'foo.ipynb', sessionId: 'sess-B',
});
@@ -1144,7 +1141,11 @@ describe('OpenCodeInlinePrompt', () => {
select.value = 'sess-B';
select.dispatchEvent(new Event('change'));
await new Promise(r => setTimeout(r, 5));
expect(mockedSetActiveSession).toHaveBeenCalled();
// bind-existing is the single call (it does both bind AND set
// active on the server). Calling set_active first would 400
// because the session is not yet bound.
expect(mockedBindExistingSession).toHaveBeenCalled();
expect(mockedSetActiveSession).not.toHaveBeenCalled();
expect((prompt as any)._sessionId).toBe('sess-B');
});
+6 -10
View File
@@ -30,7 +30,6 @@ import {
callOpenCodeReplyPermission,
callOpenCodeReplyQuestion,
callOpenCodeSessionMessages,
callOpenCodeSetActiveSession,
subscribeOpenCodeEvents,
type OpenCodeEventSubscription
} from '../api/opencode_client';
@@ -222,7 +221,12 @@ export class OpenCodeCellActions extends Widget {
// Closing the SSE first ensures the old session's events stop
// coming in while we transition.
this._closeSse();
const r = await callOpenCodeSetActiveSession(
// PUT /sessions/notebook binds AND sets active in one call.
// (The separate PUT /sessions/active is only for switching
// between sessions that are ALREADY bound; for a session the
// user just picked from the global list, we need to bind
// first — calling set_active before bind returns 400.)
const r = await callOpenCodeBindExistingSession(
this._context.notebookPath,
sessionId,
_serverSettings
@@ -230,14 +234,6 @@ export class OpenCodeCellActions extends Widget {
if (!r.ok) {
return false;
}
// The session was not necessarily bound to this notebook yet.
// Bind it (idempotent; the server's PUT /sessions/notebook
// also sets it as active, so we don't need a separate call).
await callOpenCodeBindExistingSession(
this._context.notebookPath,
sessionId,
_serverSettings
);
this._prompt?.setSessionId(sessionId);
return true;
},