Merge branch 'investigate/portless' into dev
# Conflicts: # packages/app/e2e/helpers/workspace-setup.ts # packages/app/src/components/sidebar-workspace-list.tsx # packages/app/src/contexts/session-context.tsx # packages/app/src/hooks/use-sidebar-workspaces-list.test.ts # packages/app/src/screens/workspace/workspace-tab-menu.ts # packages/app/src/stores/workspace-setup-store.ts # packages/app/src/stores/workspace-tabs-store.ts # packages/app/src/utils/sidebar-project-row-model.test.ts # packages/app/src/utils/sidebar-shortcuts.test.ts # packages/app/src/utils/workspace-tab-identity.ts # packages/server/src/server/bootstrap.ts # packages/server/src/server/worktree-session.ts
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkspaceServicePayload } from "@server/shared/messages";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { patchWorkspaceServices } from "./session-workspace-services";
|
||||
|
||||
function workspace(input: {
|
||||
id: string;
|
||||
services?: WorkspaceDescriptor["services"];
|
||||
}): WorkspaceDescriptor {
|
||||
return {
|
||||
id: input.id,
|
||||
projectId: "project-1",
|
||||
projectDisplayName: "Project 1",
|
||||
projectRootPath: "/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: "main",
|
||||
status: "running",
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
services: input.services ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
const runningService: WorkspaceServicePayload = {
|
||||
serviceName: "web",
|
||||
hostname: "main.web.localhost",
|
||||
port: 3000,
|
||||
url: "http://main.web.localhost:6767",
|
||||
status: "running",
|
||||
};
|
||||
|
||||
describe("patchWorkspaceServices", () => {
|
||||
it("patches only the matching workspace services", () => {
|
||||
const other = workspace({ id: "/repo/other", services: [] });
|
||||
const current = new Map<string, WorkspaceDescriptor>([
|
||||
["/repo/main", workspace({ id: "/repo/main", services: [] })],
|
||||
[other.id, other],
|
||||
]);
|
||||
|
||||
const next = patchWorkspaceServices(current, {
|
||||
workspaceId: "/repo/main",
|
||||
services: [runningService],
|
||||
});
|
||||
|
||||
expect(next).not.toBe(current);
|
||||
expect(next.get("/repo/main")?.services).toEqual([runningService]);
|
||||
expect(next.get("/repo/other")).toBe(other);
|
||||
});
|
||||
|
||||
it("ignores updates for unknown workspaces", () => {
|
||||
const current = new Map<string, WorkspaceDescriptor>([
|
||||
["/repo/main", workspace({ id: "/repo/main", services: [] })],
|
||||
]);
|
||||
|
||||
const next = patchWorkspaceServices(current, {
|
||||
workspaceId: "/repo/missing",
|
||||
services: [runningService],
|
||||
});
|
||||
|
||||
expect(next).toBe(current);
|
||||
expect(next.get("/repo/main")?.services).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
normalizeWorkspaceDescriptor,
|
||||
} from "@/stores/session-store";
|
||||
import { useDraftStore } from "@/stores/draft-store";
|
||||
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
|
||||
import type { AgentDirectoryEntry } from "@/types/agent-directory";
|
||||
import { sendOsNotification } from "@/utils/os-notifications";
|
||||
import { getIsAppActivelyVisible } from "@/utils/app-visibility";
|
||||
@@ -50,6 +51,7 @@ import { resolveProjectPlacement } from "@/utils/project-placement";
|
||||
import { buildDraftStoreKey } from "@/stores/draft-keys";
|
||||
import type { AttachmentMetadata } from "@/attachments/types";
|
||||
import { reconcilePreviousAgentStatuses } from "@/contexts/session-status-tracking";
|
||||
import { patchWorkspaceServices } from "@/contexts/session-workspace-services";
|
||||
|
||||
// Re-export types from session-store and draft-store for backward compatibility
|
||||
export type { DraftInput } from "@/stores/draft-store";
|
||||
@@ -159,6 +161,10 @@ type WorkspaceUpdatePayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: "workspace_update" }
|
||||
>["payload"];
|
||||
type WorkspaceSetupProgressPayload = Extract<
|
||||
SessionOutboundMessage,
|
||||
{ type: "workspace_setup_progress" }
|
||||
>["payload"];
|
||||
|
||||
const getAgentIdFromUpdate = (update: AgentUpdatePayload): string =>
|
||||
update.kind === "remove" ? update.agentId : update.agent.id;
|
||||
@@ -264,6 +270,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
|
||||
const updateSessionClient = useSessionStore((state) => state.updateSessionClient);
|
||||
const updateSessionServerInfo = useSessionStore((state) => state.updateSessionServerInfo);
|
||||
const upsertWorkspaceSetupProgress = useWorkspaceSetupStore((state) => state.upsertProgress);
|
||||
const removeWorkspaceSetup = useWorkspaceSetupStore((state) => state.removeWorkspace);
|
||||
const clearWorkspaceSetupServer = useWorkspaceSetupStore((state) => state.clearServer);
|
||||
|
||||
// Track focused agent for heartbeat
|
||||
const focusedAgentId = useSessionStore(
|
||||
@@ -747,6 +756,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
],
|
||||
);
|
||||
|
||||
const applyWorkspaceSetupProgress = useCallback(
|
||||
(payload: WorkspaceSetupProgressPayload) => {
|
||||
upsertWorkspaceSetupProgress({ serverId, payload });
|
||||
},
|
||||
[serverId, upsertWorkspaceSetupProgress],
|
||||
);
|
||||
|
||||
const requestCanonicalCatchUp = useCallback(
|
||||
(agentId: string, cursor: { endSeq: number }) => {
|
||||
void client
|
||||
@@ -1083,12 +1099,34 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
const unsubWorkspaceUpdate = client.on("workspace_update", (message) => {
|
||||
if (message.type !== "workspace_update") return;
|
||||
if (message.payload.kind === "remove") {
|
||||
removeWorkspaceSetup({ serverId, workspaceId: String(message.payload.id) });
|
||||
removeWorkspace(serverId, String(message.payload.id));
|
||||
return;
|
||||
}
|
||||
mergeWorkspaces(serverId, [normalizeWorkspaceDescriptor(message.payload.workspace)]);
|
||||
});
|
||||
|
||||
const unsubServiceStatusUpdate = client.on("service_status_update", (message) => {
|
||||
if (message.type !== "service_status_update") return;
|
||||
setWorkspaces(serverId, (prev) => patchWorkspaceServices(prev, message.payload));
|
||||
});
|
||||
|
||||
const unsubWorkspaceSetupProgress = client.on("workspace_setup_progress", (message) => {
|
||||
if (message.type !== "workspace_setup_progress") return;
|
||||
applyWorkspaceSetupProgress(message.payload);
|
||||
});
|
||||
|
||||
const unsubWorkspaceSetupStatusResponse = client.on(
|
||||
"workspace_setup_status_response",
|
||||
(message) => {
|
||||
if (message.type !== "workspace_setup_status_response") return;
|
||||
const { workspaceId, snapshot } = message.payload;
|
||||
if (snapshot) {
|
||||
applyWorkspaceSetupProgress({ workspaceId, ...snapshot });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const unsubStatus = client.on("status", (message) => {
|
||||
if (message.type !== "status") return;
|
||||
const serverInfo = parseServerInfoStatusPayload(message.payload);
|
||||
@@ -1437,6 +1475,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
unsubAgentStream();
|
||||
unsubAgentTimeline();
|
||||
unsubWorkspaceUpdate();
|
||||
unsubServiceStatusUpdate();
|
||||
unsubWorkspaceSetupProgress();
|
||||
unsubWorkspaceSetupStatusResponse();
|
||||
unsubStatus();
|
||||
unsubPermissionRequest();
|
||||
unsubPermissionResolved();
|
||||
@@ -1462,8 +1503,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
setAgentTimelineCursor,
|
||||
setInitializingAgents,
|
||||
setAgents,
|
||||
setWorkspaces,
|
||||
mergeWorkspaces,
|
||||
removeWorkspace,
|
||||
removeWorkspaceSetup,
|
||||
setAgentLastActivity,
|
||||
setPendingPermissions,
|
||||
setHasHydratedAgents,
|
||||
@@ -1471,6 +1514,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
notifyAgentAttention,
|
||||
requestCanonicalCatchUp,
|
||||
applyAgentUpdatePayload,
|
||||
applyWorkspaceSetupProgress,
|
||||
applyTimelineResponse,
|
||||
voiceRuntime,
|
||||
voiceAudioEngine,
|
||||
@@ -1674,9 +1718,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearWorkspaceSetupServer(serverId);
|
||||
clearSession(serverId);
|
||||
};
|
||||
}, [clearSession, serverId]);
|
||||
}, [clearSession, clearWorkspaceSetupServer, serverId]);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ServiceStatusUpdateMessage } from "@server/shared/messages";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
|
||||
export function patchWorkspaceServices(
|
||||
workspaces: Map<string, WorkspaceDescriptor>,
|
||||
update: ServiceStatusUpdateMessage["payload"],
|
||||
): Map<string, WorkspaceDescriptor> {
|
||||
const existing = workspaces.get(update.workspaceId);
|
||||
if (!existing) {
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
const next = new Map(workspaces);
|
||||
next.set(update.workspaceId, {
|
||||
...existing,
|
||||
services: update.services.map((s) => ({ ...s })),
|
||||
});
|
||||
return next;
|
||||
}
|
||||
Reference in New Issue
Block a user