Skip to content

Commit dbf9e3a

Browse files
efecnccursoragent
andcommitted
feat(agent-ui): hydrate active session resolve (A6.191)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent be18a9f commit dbf9e3a

5 files changed

Lines changed: 92 additions & 16 deletions

File tree

packages/agent-ui/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ consumption of shared panel shells:
306306
| A6.188 | Untitled session title check |
307307
| A6.189 | Desktop New chat uses DEFAULT_SESSION_TITLE |
308308
| A6.190 | Deleted session id blocklist ops |
309+
| A6.191 | Hydrate active session resolve |
309310

310311
### A5 complete enough
311312

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from "vitest";
2+
import { DEFAULT_SESSION_TITLE } from "../lib/backendSessionTitle.js";
3+
import {
4+
createUntitledSessionMeta,
5+
resolveActiveSessionOnHydrate,
6+
} from "../lib/resolveActiveSessionOnHydrate.js";
7+
8+
describe("resolveActiveSessionOnHydrate", () => {
9+
it("prefers activeId", () => {
10+
const sessions = [
11+
{ id: "a", title: "A" },
12+
{ id: "b", title: "B" },
13+
];
14+
const r = resolveActiveSessionOnHydrate(sessions, "b", () =>
15+
createUntitledSessionMeta("x", 1),
16+
);
17+
expect(r.active.id).toBe("b");
18+
expect(r.created).toBe(false);
19+
});
20+
21+
it("reuses untitled head", () => {
22+
const sessions = [{ id: "a", title: DEFAULT_SESSION_TITLE }];
23+
const r = resolveActiveSessionOnHydrate(sessions, null, () =>
24+
createUntitledSessionMeta("x", 1),
25+
);
26+
expect(r.active.id).toBe("a");
27+
expect(r.created).toBe(false);
28+
});
29+
30+
it("creates when none", () => {
31+
const r = resolveActiveSessionOnHydrate([], null, () =>
32+
createUntitledSessionMeta("x", 1),
33+
);
34+
expect(r.created).toBe(true);
35+
expect(r.nextSessions).toHaveLength(1);
36+
});
37+
});

packages/agent-ui/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,11 @@ export {
888888
filterDeletedSessions,
889889
appendDeletedSessionId,
890890
} from "./lib/filterDeletedSessions.js";
891+
export {
892+
resolveActiveSessionOnHydrate,
893+
createUntitledSessionMeta,
894+
} from "./lib/resolveActiveSessionOnHydrate.js";
895+
export type { SessionIdTitle } from "./lib/resolveActiveSessionOnHydrate.js";
891896
export type {
892897
UntitledSessionMeta,
893898
UntitledSessionMetaSeed,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Pure bootstrap session selection for chat hydration (A6.191).
3+
*/
4+
5+
import { isUntitledSessionTitle } from "./isUntitledSessionTitle.js";
6+
import { newUntitledSessionMeta } from "./newSessionMeta.js";
7+
8+
export type SessionIdTitle = { id: string; title: string };
9+
10+
/**
11+
* Prefer persisted activeId, else first untitled session, else mint a fresh
12+
* untitled row. Does not mutate `sessions`.
13+
*/
14+
export function resolveActiveSessionOnHydrate<T extends SessionIdTitle>(
15+
sessions: readonly T[],
16+
activeId: string | null | undefined,
17+
createUntitled: () => T,
18+
): { active: T; nextSessions: T[]; created: boolean } {
19+
let active =
20+
(activeId ? sessions.find((s) => s.id === activeId) : undefined) ?? null;
21+
if (!active && sessions[0] && isUntitledSessionTitle(sessions[0].title)) {
22+
active = sessions[0];
23+
}
24+
if (active) {
25+
return { active, nextSessions: [...sessions], created: false };
26+
}
27+
const created = createUntitled();
28+
return {
29+
active: created,
30+
nextSessions: [created, ...sessions],
31+
created: true,
32+
};
33+
}
34+
35+
/** Convenience: mint untitled meta with shared title factory. */
36+
export function createUntitledSessionMeta(id: string, now?: number) {
37+
return newUntitledSessionMeta({ id, now });
38+
}

src/modules/ai/store/chatStore.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
DEFAULT_SESSION_TITLE,
33
isUntitledSessionTitle,
44
appendDeletedSessionId,
5+
resolveActiveSessionOnHydrate,
6+
createUntitledSessionMeta,
57
} from "@altai/agent-ui";
68
import type { UIMessage } from "ai";
79
import { native } from "../lib/native";
@@ -858,22 +860,15 @@ export const useChatStore = create<StoreState>((set, get) => ({
858860
// most recent conversation instead of an empty "New chat". Else reuse the
859861
// most recent untitled "New chat" (no point stacking empty placeholders
860862
// every launch), else create a fresh one.
861-
let active =
862-
(activeId ? sessions.find((s) => s.id === activeId) : undefined) ?? null;
863-
if (!active && isUntitledSessionTitle(sessions[0]?.title)) {
864-
active = sessions[0];
865-
}
866-
let nextSessions: SessionMeta[];
867-
if (active) {
868-
nextSessions = sessions;
869-
} else {
870-
active = {
871-
id: newSessionId(),
872-
title: DEFAULT_SESSION_TITLE,
873-
createdAt: Date.now(),
874-
updatedAt: Date.now(),
875-
};
876-
nextSessions = [active, ...sessions];
863+
const resolved = resolveActiveSessionOnHydrate(
864+
sessions,
865+
activeId,
866+
() =>
867+
createUntitledSessionMeta(newSessionId()) as SessionMeta,
868+
);
869+
const active = resolved.active;
870+
const nextSessions = resolved.nextSessions as SessionMeta[];
871+
if (resolved.created) {
877872
void saveSessionsList(nextSessions);
878873
}
879874
const activeSessionId = active.id;

0 commit comments

Comments
 (0)