feat: add search param to sessions.list RPC
Server-side filtering backup for client-side session picker search. Case-insensitive substring match on displayName, label, subject, sessionId, and key. Closes #1161
This commit is contained in:
committed by
Peter Steinberger
parent
262e35c219
commit
ddb7b5c6a4
@@ -12,6 +12,7 @@ export const SessionsListParamsSchema = Type.Object(
|
|||||||
label: Type.Optional(SessionLabelString),
|
label: Type.Optional(SessionLabelString),
|
||||||
spawnedBy: Type.Optional(NonEmptyString),
|
spawnedBy: Type.Optional(NonEmptyString),
|
||||||
agentId: Type.Optional(NonEmptyString),
|
agentId: Type.Optional(NonEmptyString),
|
||||||
|
search: Type.Optional(Type.String()),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
capArrayByJsonBytes,
|
capArrayByJsonBytes,
|
||||||
classifySessionKey,
|
classifySessionKey,
|
||||||
deriveSessionTitle,
|
deriveSessionTitle,
|
||||||
|
listSessionsFromStore,
|
||||||
parseGroupKey,
|
parseGroupKey,
|
||||||
resolveGatewaySessionStoreTarget,
|
resolveGatewaySessionStoreTarget,
|
||||||
resolveSessionStoreKey,
|
resolveSessionStoreKey,
|
||||||
@@ -187,3 +188,147 @@ describe("deriveSessionTitle", () => {
|
|||||||
expect(deriveSessionTitle(entry)).toBe("Actual Subject");
|
expect(deriveSessionTitle(entry)).toBe("Actual Subject");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("listSessionsFromStore search", () => {
|
||||||
|
const baseCfg = {
|
||||||
|
session: { mainKey: "main" },
|
||||||
|
agents: { list: [{ id: "main", default: true }] },
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const makeStore = (): Record<string, SessionEntry> => ({
|
||||||
|
"agent:main:work-project": {
|
||||||
|
sessionId: "sess-work-1",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
displayName: "Work Project Alpha",
|
||||||
|
label: "work",
|
||||||
|
} as SessionEntry,
|
||||||
|
"agent:main:personal-chat": {
|
||||||
|
sessionId: "sess-personal-1",
|
||||||
|
updatedAt: Date.now() - 1000,
|
||||||
|
displayName: "Personal Chat",
|
||||||
|
subject: "Family Reunion Planning",
|
||||||
|
} as SessionEntry,
|
||||||
|
"agent:main:discord:group:dev-team": {
|
||||||
|
sessionId: "sess-discord-1",
|
||||||
|
updatedAt: Date.now() - 2000,
|
||||||
|
label: "discord",
|
||||||
|
subject: "Dev Team Discussion",
|
||||||
|
} as SessionEntry,
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns all sessions when search is empty", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns all sessions when search is undefined", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: {},
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters by displayName case-insensitively", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "WORK PROJECT" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(1);
|
||||||
|
expect(result.sessions[0].displayName).toBe("Work Project Alpha");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters by subject", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "reunion" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(1);
|
||||||
|
expect(result.sessions[0].subject).toBe("Family Reunion Planning");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters by label", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "discord" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(1);
|
||||||
|
expect(result.sessions[0].label).toBe("discord");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters by sessionId", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "sess-personal" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(1);
|
||||||
|
expect(result.sessions[0].sessionId).toBe("sess-personal-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filters by key", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "dev-team" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(1);
|
||||||
|
expect(result.sessions[0].key).toBe("agent:main:discord:group:dev-team");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns empty array when no matches", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "nonexistent-term" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matches partial strings", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: "alpha" },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(1);
|
||||||
|
expect(result.sessions[0].displayName).toBe("Work Project Alpha");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trims whitespace from search query", () => {
|
||||||
|
const store = makeStore();
|
||||||
|
const result = listSessionsFromStore({
|
||||||
|
cfg: baseCfg,
|
||||||
|
storePath: "/tmp/sessions.json",
|
||||||
|
store,
|
||||||
|
opts: { search: " personal " },
|
||||||
|
});
|
||||||
|
expect(result.sessions.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ export function listSessionsFromStore(params: {
|
|||||||
const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : "";
|
const spawnedBy = typeof opts.spawnedBy === "string" ? opts.spawnedBy : "";
|
||||||
const label = typeof opts.label === "string" ? opts.label.trim() : "";
|
const label = typeof opts.label === "string" ? opts.label.trim() : "";
|
||||||
const agentId = typeof opts.agentId === "string" ? normalizeAgentId(opts.agentId) : "";
|
const agentId = typeof opts.agentId === "string" ? normalizeAgentId(opts.agentId) : "";
|
||||||
|
const search = typeof opts.search === "string" ? opts.search.trim().toLowerCase() : "";
|
||||||
const activeMinutes =
|
const activeMinutes =
|
||||||
typeof opts.activeMinutes === "number" && Number.isFinite(opts.activeMinutes)
|
typeof opts.activeMinutes === "number" && Number.isFinite(opts.activeMinutes)
|
||||||
? Math.max(1, Math.floor(opts.activeMinutes))
|
? Math.max(1, Math.floor(opts.activeMinutes))
|
||||||
@@ -482,6 +483,13 @@ export function listSessionsFromStore(params: {
|
|||||||
})
|
})
|
||||||
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
sessions = sessions.filter((s) => {
|
||||||
|
const fields = [s.displayName, s.label, s.subject, s.sessionId, s.key];
|
||||||
|
return fields.some((f) => typeof f === "string" && f.toLowerCase().includes(search));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (activeMinutes !== undefined) {
|
if (activeMinutes !== undefined) {
|
||||||
const cutoff = now - activeMinutes * 60_000;
|
const cutoff = now - activeMinutes * 60_000;
|
||||||
sessions = sessions.filter((s) => (s.updatedAt ?? 0) >= cutoff);
|
sessions = sessions.filter((s) => (s.updatedAt ?? 0) >= cutoff);
|
||||||
|
|||||||
Reference in New Issue
Block a user