Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions packages/web-shell/client/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3532,6 +3532,161 @@ describe('App session callbacks', () => {
);
});

it('reports the selected workspace, not the stale connection workspace, when no session is active', async () => {
// A cleared session leaves connection.workspaceCwd pointing at the old
// workspace (here: a secondary with a running task). Starting a new chat
// in the primary must report the primary, not route the host back to the
// stale workspace.
mockConnection.sessionId = undefined;
mockConnection.workspaceCwd = '/work/secondary';
mockWorkspace.capabilities = {
workspaces: [
{ id: 'primary', cwd: '/workspace', primary: true, trusted: true },
{
id: 'secondary',
cwd: '/work/secondary',
primary: false,
trusted: true,
},
],
};
const onSessionIdChange = vi.fn();

renderApp({
onSessionIdChange,
initialSelectedWorkspaceCwd: '/workspace',
});
await flush();

expect(onSessionIdChange).toHaveBeenCalledWith(
undefined,
undefined,
'/workspace',
);
expect(onSessionIdChange).not.toHaveBeenCalledWith(
undefined,
'secondary',
'/work/secondary',
);
});

it('reports the primary workspace, not the stale connection workspace, when the selection is unset', async () => {
// The common new-chat path (sidebar "New task", /clear, /new) leaves
// selectedWorkspaceCwd undefined — that is how "primary" is spelled. The
// report must fall back to the primary workspace, not the stale
// connection.workspaceCwd left over from the previous session.
mockConnection.sessionId = undefined;
mockConnection.workspaceCwd = '/work/secondary';
mockWorkspace.capabilities = {
workspaces: [
{ id: 'primary', cwd: '/workspace', primary: true, trusted: true },
{
id: 'secondary',
cwd: '/work/secondary',
primary: false,
trusted: true,
},
],
};
const onSessionIdChange = vi.fn();

renderApp({ onSessionIdChange });
await flush();

expect(onSessionIdChange).toHaveBeenCalledWith(
undefined,
undefined,
'/workspace',
);
expect(onSessionIdChange).not.toHaveBeenCalledWith(
undefined,
'secondary',
'/work/secondary',
);
});

it('keeps reporting the active session workspace despite a conflicting next-session selection', async () => {
// A selection for the next session must not leak into an active session's
// report: while a session is live the host is routed by that session's own
// workspace.
mockConnection.workspaceCwd = '/work/secondary';
mockWorkspace.capabilities = {
workspaces: [
{ id: 'primary', cwd: '/workspace', primary: true, trusted: true },
{
id: 'secondary',
cwd: '/work/secondary',
primary: false,
trusted: true,
},
],
};
const onSessionIdChange = vi.fn();

renderApp({
onSessionIdChange,
initialSelectedWorkspaceCwd: '/workspace',
});
await flush();

expect(onSessionIdChange).toHaveBeenCalledWith(
'session-1',
'secondary',
'/work/secondary',
);
expect(onSessionIdChange).not.toHaveBeenCalledWith(
'session-1',
undefined,
'/workspace',
);
});

it('notifies the host on each deferred workspace switch while no session is active', async () => {
// With no active session the report must re-run when the next-session
// workspace selection changes, so a routing host follows each switch
// rather than reacting to session-id changes alone.
mockConnection.sessionId = undefined;
mockConnection.workspaceCwd = '/work/secondary';
mockWorkspace.capabilities = {
workspaces: [
{ id: 'primary', cwd: '/workspace', primary: true, trusted: true },
{
id: 'secondary',
cwd: '/work/secondary',
primary: false,
trusted: true,
},
{
id: 'tertiary',
cwd: '/work/tertiary',
primary: false,
trusted: true,
},
],
};
const onSessionIdChange = vi.fn();

renderApp({ onSessionIdChange });
await flush();

expect(onSessionIdChange).toHaveBeenLastCalledWith(
undefined,
undefined,
'/workspace',
);

act(() => {
testState.latestChatEditorProps?.onSelectWorkspace?.('/work/tertiary');
});
await flush();

expect(onSessionIdChange).toHaveBeenLastCalledWith(
undefined,
'tertiary',
'/work/tertiary',
);
});

it('creates scratch once, accepts refreshed capabilities, and opens a fresh chat', async () => {
mockWorkspace.capabilities = {
features: [
Expand Down
15 changes: 11 additions & 4 deletions packages/web-shell/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4619,8 +4619,14 @@ export function App({
lastNotifiedWorkspaceCwdRef.current = undefined;
return;
}
// After a session is cleared the connection's workspaceCwd is a leftover
// from the previous session; reporting it would misroute the host back to
// the old workspace. activeWorkspaceCwd resolves the workspace picked for
// the next session (locked / selected / primary) and is what the composer
// chip reports, so the host and the chip stay in agreement.
const reportedWorkspaceCwd = activeWorkspaceCwd ?? connection.workspaceCwd;
const activeWorkspace = workspaces.find(
(entry) => entry.cwd === connection.workspaceCwd,
(entry) => entry.cwd === reportedWorkspaceCwd,
);
if (connection.sessionId && !workspace.capabilities) return;
const workspaceId =
Expand All @@ -4630,23 +4636,24 @@ export function App({
if (
lastNotifiedSessionIdRef.current === connection.sessionId &&
lastNotifiedWorkspaceIdRef.current === workspaceId &&
lastNotifiedWorkspaceCwdRef.current === connection.workspaceCwd
lastNotifiedWorkspaceCwdRef.current === reportedWorkspaceCwd
) {
return;
}
lastNotifiedSessionIdRef.current = connection.sessionId;
lastNotifiedWorkspaceIdRef.current = workspaceId;
lastNotifiedWorkspaceCwdRef.current = connection.workspaceCwd;
lastNotifiedWorkspaceCwdRef.current = reportedWorkspaceCwd;
onSessionIdChange?.(
connection.sessionId,
workspaceId,
connection.workspaceCwd,
reportedWorkspaceCwd,
);
}, [
connection.missingSession,
connection.sessionId,
connection.workspaceCwd,
onSessionIdChange,
activeWorkspaceCwd,
workspace.capabilities,
workspaces,
]);
Expand Down
Loading