fix: SACP notifies clients of generated session names (#8983)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { useChatStore } from "@/features/chat/stores/chatStore";
|
||||
import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore";
|
||||
import { handleSessionNotification } from "../acpNotificationHandler";
|
||||
|
||||
describe("ACP session info updates", () => {
|
||||
beforeEach(() => {
|
||||
useChatStore.setState({
|
||||
messagesBySession: {},
|
||||
sessionStateById: {},
|
||||
queuedMessageBySession: {},
|
||||
draftsBySession: {},
|
||||
activeSessionId: null,
|
||||
isConnected: false,
|
||||
loadingSessionIds: new Set<string>(),
|
||||
scrollTargetMessageBySession: {},
|
||||
});
|
||||
useChatSessionStore.setState({
|
||||
sessions: [],
|
||||
activeSessionId: null,
|
||||
isLoading: false,
|
||||
hasHydratedSessions: false,
|
||||
contextPanelOpenBySession: {},
|
||||
activeWorkspaceBySession: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("applies generated session info updates to non-user-named sessions", async () => {
|
||||
useChatSessionStore.getState().addSession({
|
||||
id: "goose-session-title",
|
||||
acpSessionId: "goose-session-title",
|
||||
title: "New Chat",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
messageCount: 0,
|
||||
userSetName: false,
|
||||
});
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session-title",
|
||||
update: {
|
||||
sessionUpdate: "session_info_update",
|
||||
title: "Generated Test Title",
|
||||
updatedAt: "2026-01-01T00:01:00.000Z",
|
||||
_meta: {
|
||||
messageCount: 1,
|
||||
userSetName: false,
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(
|
||||
useChatSessionStore.getState().getSession("goose-session-title"),
|
||||
).toMatchObject({
|
||||
title: "Generated Test Title",
|
||||
updatedAt: "2026-01-01T00:01:00.000Z",
|
||||
messageCount: 1,
|
||||
userSetName: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores generated titles for user-named sessions", async () => {
|
||||
useChatSessionStore.getState().addSession({
|
||||
id: "goose-session-user-title",
|
||||
acpSessionId: "goose-session-user-title",
|
||||
title: "My Custom Title",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
messageCount: 0,
|
||||
userSetName: true,
|
||||
});
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session-user-title",
|
||||
update: {
|
||||
sessionUpdate: "session_info_update",
|
||||
title: "Generated Test Title",
|
||||
updatedAt: "2026-01-01T00:01:00.000Z",
|
||||
_meta: {
|
||||
messageCount: 1,
|
||||
userSetName: true,
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(
|
||||
useChatSessionStore.getState().getSession("goose-session-user-title"),
|
||||
).toMatchObject({
|
||||
title: "My Custom Title",
|
||||
updatedAt: "2026-01-01T00:01:00.000Z",
|
||||
messageCount: 1,
|
||||
userSetName: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
getTrackedReplayAssistantMessageId,
|
||||
} from "./acpReplayAssistant";
|
||||
import { getReplayCreated, getReplayMessageId } from "./acpReplayMetadata";
|
||||
import { handleSessionInfoUpdate } from "./acpSessionInfoUpdate";
|
||||
import {
|
||||
getLocalSessionId,
|
||||
subscribeToSessionRegistration,
|
||||
@@ -487,17 +488,7 @@ function handleShared(
|
||||
): void {
|
||||
switch (update.sessionUpdate) {
|
||||
case "session_info_update": {
|
||||
const info = update as SessionUpdate & {
|
||||
sessionUpdate: "session_info_update";
|
||||
};
|
||||
if ("title" in info && info.title) {
|
||||
const session = useChatSessionStore.getState().getSession(sessionId);
|
||||
if (session && !session.userSetName) {
|
||||
useChatSessionStore
|
||||
.getState()
|
||||
.updateSession(sessionId, { title: info.title as string });
|
||||
}
|
||||
}
|
||||
handleSessionInfoUpdate(sessionId, update);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { SessionUpdate } from "@agentclientprotocol/sdk";
|
||||
import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore";
|
||||
|
||||
type SessionInfoUpdate = SessionUpdate & {
|
||||
sessionUpdate: "session_info_update";
|
||||
title?: unknown;
|
||||
updatedAt?: unknown;
|
||||
_meta?: unknown;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
export function handleSessionInfoUpdate(
|
||||
sessionId: string,
|
||||
update: SessionUpdate,
|
||||
): void {
|
||||
const info = update as SessionInfoUpdate;
|
||||
const sessionStore = useChatSessionStore.getState();
|
||||
const session = sessionStore.getSession(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = isRecord(info._meta) ? info._meta : {};
|
||||
const patch: Parameters<typeof sessionStore.updateSession>[1] = {};
|
||||
|
||||
if (typeof info.title === "string" && info.title && !session.userSetName) {
|
||||
patch.title = info.title;
|
||||
}
|
||||
if (typeof info.updatedAt === "string" && info.updatedAt) {
|
||||
patch.updatedAt = info.updatedAt;
|
||||
}
|
||||
if (typeof meta.messageCount === "number") {
|
||||
patch.messageCount = meta.messageCount;
|
||||
}
|
||||
if (typeof meta.userSetName === "boolean") {
|
||||
patch.userSetName = meta.userSetName;
|
||||
}
|
||||
|
||||
if (Object.keys(patch).length > 0) {
|
||||
sessionStore.updateSession(sessionId, patch);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user