Fix notifications opening the wrong workspace (#2331)

* fix(app): open notifications on the right host

Agent notifications previously omitted workspace ownership, so a cold target host was treated as missing and fell back to its empty home route. Carry the authoritative workspace and keep older notifications on a target-host resolver until lookup is conclusive.

* fix(app): separate notification and agent URL routing

Notifications use their authoritative workspace target directly. Stable agent URLs remain server-and-agent targets whose workspace is resolved by the agent route.

* fix(protocol): preserve notification workspace targets

Both attention message variants retain workspaceId through outbound validation so notification clicks receive the authoritative route target.

* test(app): use workspace-scoped notification target

* test(server): give notification agents workspace targets

* test(app): target notification workspace in navigation e2e
This commit is contained in:
Mohamed Boudra
2026-07-23 03:32:28 +08:00
committed by GitHub
parent de69b2a2af
commit 5dfe50ca74
19 changed files with 627 additions and 110 deletions
@@ -14,6 +14,8 @@ import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js
import type { PushNotificationSender, PushPayload } from "./push/notifications.js";
import type { WorkspaceAutoName } from "./workspace-auto-name.js";
const WORKSPACE_ID = "workspace-1";
const wsModuleMock = vi.hoisted(() => {
class MockWebSocketServer {
readonly handlers = new Map<string, (...args: unknown[]) => void>();
@@ -86,7 +88,7 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
const agentManager = {
subscribe: vi.fn(() => () => {}),
setAgentAttentionCallback: vi.fn(),
getAgent: vi.fn(() => null),
getAgent: vi.fn(() => ({ workspaceId: WORKSPACE_ID, pendingPermissions: new Map() })),
getLastAssistantMessage: vi.fn(async () => null),
getMetricsSnapshot: vi.fn(() => ({
total: 0,
@@ -225,6 +227,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
getAgent: vi.fn(() => ({
config: { title: null },
cwd: "/tmp/worktree",
workspaceId: WORKSPACE_ID,
pendingPermissions: new Map(),
})),
getLastAssistantMessage,
@@ -242,6 +245,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
body: "Done. Updated README.md and link.",
data: {
serverId: "srv-test",
workspaceId: WORKSPACE_ID,
agentId: "agent-1",
reason: "finished",
},
@@ -256,6 +260,7 @@ describe("VoiceAssistantWebSocketServer notification payloads", () => {
getAgent: vi.fn(() => ({
config: { title: null },
cwd: "/tmp/worktree",
workspaceId: WORKSPACE_ID,
labels: {},
pendingPermissions: new Map(),
})),
@@ -2152,13 +2152,17 @@ export class VoiceAssistantWebSocketServer {
const allStates = clientEntries.map((e) => e.state);
const nowMs = Date.now();
const agent = this.agentManager.getAgent(params.agentId);
if (!agent?.workspaceId) {
return;
}
const assistantMessage = await this.agentManager.getLastAssistantMessage(params.agentId);
const notification = buildAgentAttentionNotificationPayload({
reason: params.reason,
serverId: this.serverId,
workspaceId: agent.workspaceId,
agentId: params.agentId,
assistantMessage,
permissionRequest: agent ? findLatestPermissionRequest(agent.pendingPermissions) : null,
permissionRequest: findLatestPermissionRequest(agent.pendingPermissions),
});
const plan = computeNotificationPlan({