Skip to content

Commit 6056f2b

Browse files
committed
fix: open chat directly when workspace is already open via deeplink
When a deeplink with chatId targets an already-open workspace, VS Code refocuses without reloading so activate() never consumes the pending chatId. Call openChat directly after commands.open() as a fallback. Also refactors registerUriHandler to accept a single deps object and simplifies chatPanelProvider tests.
1 parent d1ff81d commit 6056f2b

File tree

4 files changed

+121
-111
lines changed

4 files changed

+121
-111
lines changed

src/extension.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
238238
);
239239

240240
ctx.subscriptions.push(
241-
registerUriHandler(serviceContainer, deploymentManager, commands),
241+
registerUriHandler({
242+
serviceContainer,
243+
deploymentManager,
244+
commands,
245+
chatPanelProvider,
246+
}),
242247
vscode.commands.registerCommand(
243248
"coder.login",
244249
commands.login.bind(commands),

src/uri/uriHandler.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
import * as vscode from "vscode";
22

33
import { errToStr } from "../api/api-helper";
4-
import { type Commands } from "../commands";
5-
import { type ServiceContainer } from "../core/container";
6-
import { type DeploymentManager } from "../deployment/deploymentManager";
74
import { CALLBACK_PATH } from "../oauth/utils";
85
import { maybeAskUrl } from "../promptUtils";
96
import { toSafeHost } from "../util";
107
import { vscodeProposed } from "../vscodeProposed";
118

12-
interface UriRouteContext {
13-
params: URLSearchParams;
9+
import type { Commands } from "../commands";
10+
import type { ServiceContainer } from "../core/container";
11+
import type { DeploymentManager } from "../deployment/deploymentManager";
12+
import type { ChatPanelProvider } from "../webviews/chat/chatPanelProvider";
13+
14+
interface UriHandlerDeps {
1415
serviceContainer: ServiceContainer;
1516
deploymentManager: DeploymentManager;
1617
commands: Commands;
18+
chatPanelProvider: ChatPanelProvider;
19+
}
20+
21+
interface UriRouteContext extends UriHandlerDeps {
22+
params: URLSearchParams;
1723
}
1824

1925
type UriRouteHandler = (ctx: UriRouteContext) => Promise<void>;
@@ -27,17 +33,20 @@ const routes: Readonly<Record<string, UriRouteHandler>> = {
2733
/**
2834
* Registers the URI handler for `{vscode.env.uriScheme}://coder.coder-remote`... URIs.
2935
*/
30-
export function registerUriHandler(
31-
serviceContainer: ServiceContainer,
32-
deploymentManager: DeploymentManager,
33-
commands: Commands,
34-
): vscode.Disposable {
35-
const output = serviceContainer.getLogger();
36+
export function registerUriHandler(deps: UriHandlerDeps): vscode.Disposable {
37+
const output = deps.serviceContainer.getLogger();
3638

3739
return vscode.window.registerUriHandler({
3840
handleUri: async (uri) => {
3941
try {
40-
await routeUri(uri, serviceContainer, deploymentManager, commands);
42+
const handler = routes[uri.path];
43+
if (!handler) {
44+
throw new Error(`Unknown path ${uri.path}`);
45+
}
46+
await handler({
47+
...deps,
48+
params: new URLSearchParams(uri.query),
49+
});
4150
} catch (error) {
4251
const message = errToStr(error, "No error message was provided");
4352
output.warn(`Failed to handle URI ${uri.toString()}: ${message}`);
@@ -51,25 +60,6 @@ export function registerUriHandler(
5160
});
5261
}
5362

54-
async function routeUri(
55-
uri: vscode.Uri,
56-
serviceContainer: ServiceContainer,
57-
deploymentManager: DeploymentManager,
58-
commands: Commands,
59-
): Promise<void> {
60-
const handler = routes[uri.path];
61-
if (!handler) {
62-
throw new Error(`Unknown path ${uri.path}`);
63-
}
64-
65-
await handler({
66-
params: new URLSearchParams(uri.query),
67-
serviceContainer,
68-
deploymentManager,
69-
commands,
70-
});
71-
}
72-
7363
function getRequiredParam(params: URLSearchParams, name: string): string {
7464
const value = params.get(name);
7565
if (!value) {
@@ -116,6 +106,13 @@ async function handleOpen(ctx: UriRouteContext): Promise<void> {
116106
await mementoManager.clearPendingChatId();
117107
}
118108
}
109+
110+
// When the workspace is already open VS Code refocuses without
111+
// reloading, so activate() won't consume the pending chatId.
112+
if (opened && chatId) {
113+
serviceContainer.getContextManager().set("coder.agentsEnabled", true);
114+
ctx.chatPanelProvider.openChat(chatId);
115+
}
119116
}
120117

121118
async function handleOpenDevContainer(ctx: UriRouteContext): Promise<void> {

test/unit/uri/uriHandler.test.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import {
1212
createMockUser,
1313
InMemoryMemento,
1414
InMemorySecretStorage,
15+
MockContextManager,
1516
} from "../../mocks/testHelpers";
1617

1718
import type { Commands } from "@/commands";
1819
import type { ServiceContainer } from "@/core/container";
1920
import type { DeploymentManager } from "@/deployment/deploymentManager";
2021
import type { LoginCoordinator, LoginOptions } from "@/login/loginCoordinator";
22+
import type { ChatPanelProvider } from "@/webviews/chat/chatPanelProvider";
2123

2224
vi.mock("@/promptUtils", () => ({ maybeAskUrl: vi.fn() }));
2325

@@ -77,6 +79,7 @@ function createTestContext() {
7779
getSecretsManager: () => secretsManager,
7880
getMementoManager: () => mementoManager,
7981
getLoginCoordinator: () => loginCoordinator as unknown as LoginCoordinator,
82+
getContextManager: () => new MockContextManager(),
8083
getLogger: () => logger,
8184
} as unknown as ServiceContainer;
8285

@@ -94,11 +97,16 @@ function createTestContext() {
9497
.mocked(vscode.window.showErrorMessage)
9598
.mockResolvedValue(undefined);
9699

97-
registerUriHandler(
98-
container,
99-
deploymentManager as unknown as DeploymentManager,
100-
commands as unknown as Commands,
101-
);
100+
const chatPanelProvider = {
101+
openChat: vi.fn(),
102+
} as unknown as ChatPanelProvider;
103+
104+
registerUriHandler({
105+
serviceContainer: container,
106+
deploymentManager: deploymentManager as unknown as DeploymentManager,
107+
commands: commands as unknown as Commands,
108+
chatPanelProvider,
109+
});
102110

103111
return {
104112
commands,
@@ -107,6 +115,7 @@ function createTestContext() {
107115
secretsManager,
108116
logger,
109117
showErrorMessage,
118+
chatPanelProvider,
110119
handleUri: registeredHandler!,
111120
};
112121
}
@@ -150,6 +159,25 @@ describe("uriHandler", () => {
150159
expected,
151160
);
152161
});
162+
163+
it("opens chat when chatId is present and open succeeds", async () => {
164+
const { handleUri, commands, chatPanelProvider } = createTestContext();
165+
commands.open.mockResolvedValue(true);
166+
const query = `owner=o&workspace=w&chatId=chat-123&url=${encodeURIComponent(TEST_URL)}`;
167+
await handleUri(createMockUri("/open", query));
168+
expect(chatPanelProvider.openChat).toHaveBeenCalledWith("chat-123");
169+
});
170+
171+
it.each([
172+
["no chatId", "owner=o&workspace=w", true],
173+
["open returns false", "owner=o&workspace=w&chatId=chat-123", false],
174+
])("does not open chat when %s", async (_label, params, openResult) => {
175+
const { handleUri, commands, chatPanelProvider } = createTestContext();
176+
commands.open.mockResolvedValue(openResult);
177+
const query = `${params}&url=${encodeURIComponent(TEST_URL)}`;
178+
await handleUri(createMockUri("/open", query));
179+
expect(chatPanelProvider.openChat).not.toHaveBeenCalled();
180+
});
153181
});
154182

155183
describe("/openDevContainer", () => {

0 commit comments

Comments
 (0)