mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
feat: add session library backend APIs
Add persisted session and pattern metadata for pinning, archiving, manual titles, and favorite patterns. Expose backend query and mutation APIs for session library features, and cover the shared helpers with tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -15,17 +15,24 @@ import {
|
||||
resolveReasoningEffort,
|
||||
} from '@shared/domain/models';
|
||||
import {
|
||||
buildSessionTitle,
|
||||
isReasoningEffort,
|
||||
type PatternDefinition,
|
||||
type ReasoningEffort,
|
||||
validatePatternDefinition,
|
||||
} from '@shared/domain/pattern';
|
||||
import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
|
||||
import {
|
||||
duplicateSessionRecord,
|
||||
querySessions as queryWorkspaceSessions,
|
||||
renameSessionRecord,
|
||||
type QuerySessionsInput,
|
||||
type SessionQueryResult,
|
||||
} from '@shared/domain/sessionLibrary';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import {
|
||||
applyScratchpadSessionConfig,
|
||||
createScratchpadSessionConfig,
|
||||
resolveSessionTitle,
|
||||
type ChatMessageRecord,
|
||||
type SessionRecord,
|
||||
} from '@shared/domain/session';
|
||||
@@ -137,6 +144,7 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
const existingIndex = workspace.patterns.findIndex((current) => current.id === pattern.id);
|
||||
const candidate: PatternDefinition = {
|
||||
...pattern,
|
||||
isFavorite: pattern.isFavorite ?? workspace.patterns[existingIndex]?.isFavorite,
|
||||
createdAt: existingIndex >= 0 ? workspace.patterns[existingIndex].createdAt : nowIso(),
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
@@ -151,6 +159,14 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async setPatternFavorite(patternId: string, isFavorite: boolean): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const pattern = this.requirePattern(workspace, patternId);
|
||||
pattern.isFavorite = isFavorite;
|
||||
pattern.updatedAt = nowIso();
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async deletePattern(patternId: string): Promise<WorkspaceState> {
|
||||
if (isBuiltinPattern(patternId)) {
|
||||
throw new Error('Built-in patterns cannot be deleted.');
|
||||
@@ -178,6 +194,7 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
projectId: project.id,
|
||||
patternId: pattern.id,
|
||||
title: pattern.name,
|
||||
titleSource: 'auto',
|
||||
createdAt: nowIso(),
|
||||
updatedAt: nowIso(),
|
||||
status: 'idle',
|
||||
@@ -194,6 +211,43 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async duplicateSession(sessionId: string): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
const duplicate = duplicateSessionRecord(session, createId('session'), nowIso());
|
||||
|
||||
workspace.sessions.unshift(duplicate);
|
||||
workspace.selectedProjectId = duplicate.projectId;
|
||||
workspace.selectedPatternId = duplicate.patternId;
|
||||
workspace.selectedSessionId = duplicate.id;
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async renameSession(sessionId: string, title: string): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
const renamed = renameSessionRecord(session, title, nowIso());
|
||||
|
||||
Object.assign(session, renamed);
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async setSessionPinned(sessionId: string, isPinned: boolean): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
session.isPinned = isPinned;
|
||||
session.updatedAt = nowIso();
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async setSessionArchived(sessionId: string, isArchived: boolean): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
session.isArchived = isArchived;
|
||||
session.updatedAt = nowIso();
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async sendSessionMessage(sessionId: string, content: string): Promise<void> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
@@ -213,7 +267,7 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
content: trimmed,
|
||||
createdAt: nowIso(),
|
||||
});
|
||||
session.title = buildSessionTitle(effectivePattern, session.messages);
|
||||
session.title = resolveSessionTitle(session, effectivePattern, session.messages);
|
||||
session.status = 'running';
|
||||
session.lastError = undefined;
|
||||
session.updatedAt = nowIso();
|
||||
@@ -301,6 +355,11 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async querySessions(input: QuerySessionsInput): Promise<SessionQueryResult[]> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
return queryWorkspaceSessions(workspace, input);
|
||||
}
|
||||
|
||||
async selectProject(projectId?: string): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
workspace.selectedProjectId = projectId;
|
||||
|
||||
@@ -3,10 +3,16 @@ import { BrowserWindow, ipcMain } from 'electron';
|
||||
import { ipcChannels } from '@shared/contracts/channels';
|
||||
import type {
|
||||
CreateSessionInput,
|
||||
DuplicateSessionInput,
|
||||
RenameSessionInput,
|
||||
SavePatternInput,
|
||||
SendSessionMessageInput,
|
||||
SetPatternFavoriteInput,
|
||||
SetSessionArchivedInput,
|
||||
SetSessionPinnedInput,
|
||||
UpdateScratchpadSessionConfigInput,
|
||||
} from '@shared/contracts/ipc';
|
||||
import type { QuerySessionsInput } from '@shared/domain/sessionLibrary';
|
||||
|
||||
import { KopayaAppService } from '@main/KopayaAppService';
|
||||
|
||||
@@ -18,9 +24,24 @@ export function registerIpcHandlers(window: BrowserWindow, service: KopayaAppSer
|
||||
ipcMain.handle(ipcChannels.removeProject, (_event, projectId: string) => service.removeProject(projectId));
|
||||
ipcMain.handle(ipcChannels.savePattern, (_event, input: SavePatternInput) => service.savePattern(input.pattern));
|
||||
ipcMain.handle(ipcChannels.deletePattern, (_event, patternId: string) => service.deletePattern(patternId));
|
||||
ipcMain.handle(ipcChannels.setPatternFavorite, (_event, input: SetPatternFavoriteInput) =>
|
||||
service.setPatternFavorite(input.patternId, input.isFavorite),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.createSession, (_event, input: CreateSessionInput) =>
|
||||
service.createSession(input.projectId, input.patternId),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.duplicateSession, (_event, input: DuplicateSessionInput) =>
|
||||
service.duplicateSession(input.sessionId),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.renameSession, (_event, input: RenameSessionInput) =>
|
||||
service.renameSession(input.sessionId, input.title),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.setSessionPinned, (_event, input: SetSessionPinnedInput) =>
|
||||
service.setSessionPinned(input.sessionId, input.isPinned),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.setSessionArchived, (_event, input: SetSessionArchivedInput) =>
|
||||
service.setSessionArchived(input.sessionId, input.isArchived),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.sendSessionMessage, (_event, input: SendSessionMessageInput) =>
|
||||
service.sendSessionMessage(input.sessionId, input.content),
|
||||
);
|
||||
@@ -29,6 +50,7 @@ export function registerIpcHandlers(window: BrowserWindow, service: KopayaAppSer
|
||||
(_event, input: UpdateScratchpadSessionConfigInput) =>
|
||||
service.updateScratchpadSessionConfig(input.sessionId, input.model, input.reasoningEffort),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.querySessions, (_event, input: QuerySessionsInput) => service.querySessions(input));
|
||||
ipcMain.handle(ipcChannels.selectProject, (_event, projectId?: string) => service.selectProject(projectId));
|
||||
ipcMain.handle(ipcChannels.selectPattern, (_event, patternId?: string) => service.selectPattern(patternId));
|
||||
ipcMain.handle(ipcChannels.selectSession, (_event, sessionId?: string) => service.selectSession(sessionId));
|
||||
|
||||
@@ -11,10 +11,16 @@ const api: ElectronApi = {
|
||||
removeProject: (projectId) => ipcRenderer.invoke(ipcChannels.removeProject, projectId),
|
||||
savePattern: (input) => ipcRenderer.invoke(ipcChannels.savePattern, input),
|
||||
deletePattern: (patternId) => ipcRenderer.invoke(ipcChannels.deletePattern, patternId),
|
||||
setPatternFavorite: (input) => ipcRenderer.invoke(ipcChannels.setPatternFavorite, input),
|
||||
createSession: (input) => ipcRenderer.invoke(ipcChannels.createSession, input),
|
||||
duplicateSession: (input) => ipcRenderer.invoke(ipcChannels.duplicateSession, input),
|
||||
renameSession: (input) => ipcRenderer.invoke(ipcChannels.renameSession, input),
|
||||
setSessionPinned: (input) => ipcRenderer.invoke(ipcChannels.setSessionPinned, input),
|
||||
setSessionArchived: (input) => ipcRenderer.invoke(ipcChannels.setSessionArchived, input),
|
||||
sendSessionMessage: (input) => ipcRenderer.invoke(ipcChannels.sendSessionMessage, input),
|
||||
updateScratchpadSessionConfig: (input) =>
|
||||
ipcRenderer.invoke(ipcChannels.updateScratchpadSessionConfig, input),
|
||||
querySessions: (input) => ipcRenderer.invoke(ipcChannels.querySessions, input),
|
||||
selectProject: (projectId) => ipcRenderer.invoke(ipcChannels.selectProject, projectId),
|
||||
selectPattern: (patternId) => ipcRenderer.invoke(ipcChannels.selectPattern, patternId),
|
||||
selectSession: (sessionId) => ipcRenderer.invoke(ipcChannels.selectSession, sessionId),
|
||||
|
||||
@@ -6,8 +6,14 @@ export const ipcChannels = {
|
||||
removeProject: 'workspace:remove-project',
|
||||
savePattern: 'patterns:save',
|
||||
deletePattern: 'patterns:delete',
|
||||
setPatternFavorite: 'patterns:set-favorite',
|
||||
createSession: 'sessions:create',
|
||||
duplicateSession: 'sessions:duplicate',
|
||||
renameSession: 'sessions:rename',
|
||||
setSessionPinned: 'sessions:set-pinned',
|
||||
setSessionArchived: 'sessions:set-archived',
|
||||
sendSessionMessage: 'sessions:send-message',
|
||||
querySessions: 'sessions:query',
|
||||
updateScratchpadSessionConfig: 'sessions:update-scratchpad-config',
|
||||
selectProject: 'selection:project',
|
||||
selectPattern: 'selection:pattern',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SidecarCapabilities } from '@shared/contracts/sidecar';
|
||||
import type { PatternDefinition, ReasoningEffort } from '@shared/domain/pattern';
|
||||
import type { ProjectRecord } from '@shared/domain/project';
|
||||
import type { QuerySessionsInput, SessionQueryResult } from '@shared/domain/sessionLibrary';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
|
||||
@@ -24,6 +25,30 @@ export interface UpdateScratchpadSessionConfigInput {
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
}
|
||||
|
||||
export interface DuplicateSessionInput {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface RenameSessionInput {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface SetSessionPinnedInput {
|
||||
sessionId: string;
|
||||
isPinned: boolean;
|
||||
}
|
||||
|
||||
export interface SetSessionArchivedInput {
|
||||
sessionId: string;
|
||||
isArchived: boolean;
|
||||
}
|
||||
|
||||
export interface SetPatternFavoriteInput {
|
||||
patternId: string;
|
||||
isFavorite: boolean;
|
||||
}
|
||||
|
||||
export interface ElectronApi {
|
||||
describeSidecarCapabilities(): Promise<SidecarCapabilities>;
|
||||
refreshSidecarCapabilities(): Promise<SidecarCapabilities>;
|
||||
@@ -33,11 +58,17 @@ export interface ElectronApi {
|
||||
savePattern(input: SavePatternInput): Promise<WorkspaceState>;
|
||||
deletePattern(patternId: string): Promise<WorkspaceState>;
|
||||
createSession(input: CreateSessionInput): Promise<WorkspaceState>;
|
||||
duplicateSession(input: DuplicateSessionInput): Promise<WorkspaceState>;
|
||||
renameSession(input: RenameSessionInput): Promise<WorkspaceState>;
|
||||
setSessionPinned(input: SetSessionPinnedInput): Promise<WorkspaceState>;
|
||||
setSessionArchived(input: SetSessionArchivedInput): Promise<WorkspaceState>;
|
||||
sendSessionMessage(input: SendSessionMessageInput): Promise<void>;
|
||||
updateScratchpadSessionConfig(input: UpdateScratchpadSessionConfigInput): Promise<WorkspaceState>;
|
||||
querySessions(input: QuerySessionsInput): Promise<SessionQueryResult[]>;
|
||||
selectProject(projectId?: string): Promise<WorkspaceState>;
|
||||
selectPattern(patternId?: string): Promise<WorkspaceState>;
|
||||
selectSession(sessionId?: string): Promise<WorkspaceState>;
|
||||
setPatternFavorite(input: SetPatternFavoriteInput): Promise<WorkspaceState>;
|
||||
onWorkspaceUpdated(listener: (workspace: WorkspaceState) => void): () => void;
|
||||
onSessionEvent(listener: (event: SessionEventRecord) => void): () => void;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface PatternDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isFavorite?: boolean;
|
||||
mode: OrchestrationMode;
|
||||
availability: PatternAvailability;
|
||||
unavailabilityReason?: string;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { PatternDefinition, ReasoningEffort } from '@shared/domain/pattern';
|
||||
import { buildSessionTitle, type PatternDefinition, type ReasoningEffort } from '@shared/domain/pattern';
|
||||
|
||||
export type ChatRole = 'system' | 'user' | 'assistant';
|
||||
export type SessionStatus = 'idle' | 'running' | 'error';
|
||||
export type SessionTitleSource = 'auto' | 'manual';
|
||||
|
||||
export interface ScratchpadSessionConfig {
|
||||
model: string;
|
||||
@@ -22,14 +23,29 @@ export interface SessionRecord {
|
||||
projectId: string;
|
||||
patternId: string;
|
||||
title: string;
|
||||
titleSource?: SessionTitleSource;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
status: SessionStatus;
|
||||
isPinned?: boolean;
|
||||
isArchived?: boolean;
|
||||
messages: ChatMessageRecord[];
|
||||
lastError?: string;
|
||||
scratchpadConfig?: ScratchpadSessionConfig;
|
||||
}
|
||||
|
||||
export function resolveSessionTitle(
|
||||
session: Pick<SessionRecord, 'title' | 'titleSource'>,
|
||||
pattern: PatternDefinition,
|
||||
messages: ChatMessageRecord[],
|
||||
): string {
|
||||
if (session.titleSource === 'manual') {
|
||||
return session.title;
|
||||
}
|
||||
|
||||
return buildSessionTitle(pattern, messages);
|
||||
}
|
||||
|
||||
export function createScratchpadSessionConfig(
|
||||
pattern: PatternDefinition,
|
||||
): ScratchpadSessionConfig | undefined {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
|
||||
import type { ChatMessageRecord, SessionRecord, SessionStatus } from '@shared/domain/session';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
|
||||
export type SessionQueryMatchField = 'title' | 'message' | 'project' | 'pattern';
|
||||
export type SessionWorkspaceKind = 'project' | 'scratchpad';
|
||||
|
||||
export interface QuerySessionsInput {
|
||||
searchText?: string;
|
||||
statuses?: SessionStatus[];
|
||||
projectIds?: string[];
|
||||
patternIds?: string[];
|
||||
workspaceKinds?: SessionWorkspaceKind[];
|
||||
includeArchived?: boolean;
|
||||
onlyPinned?: boolean;
|
||||
}
|
||||
|
||||
export interface SessionQueryResult {
|
||||
sessionId: string;
|
||||
score: number;
|
||||
matchedFields: SessionQueryMatchField[];
|
||||
}
|
||||
|
||||
const fieldWeights: Record<SessionQueryMatchField, number> = {
|
||||
title: 12,
|
||||
project: 8,
|
||||
pattern: 7,
|
||||
message: 4,
|
||||
};
|
||||
|
||||
const orderedMatchFields: SessionQueryMatchField[] = ['title', 'message', 'project', 'pattern'];
|
||||
|
||||
interface SessionSearchFields {
|
||||
title: string;
|
||||
message: string;
|
||||
project: string;
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
function normalizeSearchText(value: string): string {
|
||||
return value.trim().toLocaleLowerCase();
|
||||
}
|
||||
|
||||
function tokenizeSearchText(value?: string): string[] {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return normalizeSearchText(value)
|
||||
.split(/\s+/)
|
||||
.filter((token) => token.length > 0);
|
||||
}
|
||||
|
||||
function buildSearchFields(
|
||||
session: SessionRecord,
|
||||
project?: ProjectRecord,
|
||||
pattern?: PatternDefinition,
|
||||
): SessionSearchFields {
|
||||
return {
|
||||
title: normalizeSearchText(session.title),
|
||||
message: normalizeSearchText(session.messages.map((message) => message.content).join('\n')),
|
||||
project: normalizeSearchText([project?.name, project?.path].filter(Boolean).join('\n')),
|
||||
pattern: normalizeSearchText(
|
||||
[
|
||||
pattern?.name,
|
||||
pattern?.description,
|
||||
pattern?.mode,
|
||||
...(pattern?.agents.flatMap((agent) => [agent.name, agent.description]) ?? []),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSearchMatch(
|
||||
fields: SessionSearchFields,
|
||||
tokens: string[],
|
||||
): Pick<SessionQueryResult, 'score' | 'matchedFields'> | undefined {
|
||||
if (tokens.length === 0) {
|
||||
return {
|
||||
score: 0,
|
||||
matchedFields: [],
|
||||
};
|
||||
}
|
||||
|
||||
const matched = new Set<SessionQueryMatchField>();
|
||||
let score = 0;
|
||||
|
||||
for (const token of tokens) {
|
||||
let tokenMatched = false;
|
||||
|
||||
for (const field of orderedMatchFields) {
|
||||
if (!fields[field].includes(token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tokenMatched = true;
|
||||
matched.add(field);
|
||||
score += fieldWeights[field];
|
||||
}
|
||||
|
||||
if (!tokenMatched) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
matchedFields: orderedMatchFields.filter((field) => matched.has(field)),
|
||||
};
|
||||
}
|
||||
|
||||
function matchesFilters(
|
||||
session: SessionRecord,
|
||||
input: QuerySessionsInput,
|
||||
workspaceKind: SessionWorkspaceKind,
|
||||
): boolean {
|
||||
if (!input.includeArchived && session.isArchived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.onlyPinned && !session.isPinned) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.statuses && input.statuses.length > 0 && !input.statuses.includes(session.status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.projectIds && input.projectIds.length > 0 && !input.projectIds.includes(session.projectId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.patternIds && input.patternIds.length > 0 && !input.patternIds.includes(session.patternId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.workspaceKinds && input.workspaceKinds.length > 0 && !input.workspaceKinds.includes(workspaceKind)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function renameSessionRecord(session: SessionRecord, title: string, updatedAt: string): SessionRecord {
|
||||
const trimmedTitle = title.trim();
|
||||
if (!trimmedTitle) {
|
||||
throw new Error('Session title is required.');
|
||||
}
|
||||
|
||||
return {
|
||||
...session,
|
||||
title: trimmedTitle,
|
||||
titleSource: 'manual',
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function duplicateSessionRecord(
|
||||
session: SessionRecord,
|
||||
sessionId: string,
|
||||
duplicatedAt: string,
|
||||
): SessionRecord {
|
||||
return {
|
||||
...session,
|
||||
id: sessionId,
|
||||
title: `${session.title} (Copy)`,
|
||||
titleSource: 'manual',
|
||||
createdAt: duplicatedAt,
|
||||
updatedAt: duplicatedAt,
|
||||
status: 'idle',
|
||||
isPinned: false,
|
||||
isArchived: false,
|
||||
lastError: undefined,
|
||||
scratchpadConfig: session.scratchpadConfig ? { ...session.scratchpadConfig } : undefined,
|
||||
messages: session.messages.map((message): ChatMessageRecord => ({
|
||||
...message,
|
||||
pending: false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function querySessions(workspace: WorkspaceState, input: QuerySessionsInput): SessionQueryResult[] {
|
||||
const projectsById = new Map<string, ProjectRecord>(workspace.projects.map((project) => [project.id, project]));
|
||||
const patternsById = new Map<string, PatternDefinition>(workspace.patterns.map((pattern) => [pattern.id, pattern]));
|
||||
const searchTokens = tokenizeSearchText(input.searchText);
|
||||
|
||||
return workspace.sessions
|
||||
.flatMap((session) => {
|
||||
const workspaceKind: SessionWorkspaceKind = isScratchpadProject(session.projectId) ? 'scratchpad' : 'project';
|
||||
if (!matchesFilters(session, input, workspaceKind)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchMatch = resolveSearchMatch(
|
||||
buildSearchFields(session, projectsById.get(session.projectId), patternsById.get(session.patternId)),
|
||||
searchTokens,
|
||||
);
|
||||
if (!searchMatch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
sessionId: session.id,
|
||||
score: searchMatch.score,
|
||||
matchedFields: searchMatch.matchedFields,
|
||||
},
|
||||
];
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const leftSession = workspace.sessions.find((session) => session.id === left.sessionId);
|
||||
const rightSession = workspace.sessions.find((session) => session.id === right.sessionId);
|
||||
if (!leftSession || !rightSession) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (leftSession.isPinned !== rightSession.isPinned) {
|
||||
return leftSession.isPinned ? -1 : 1;
|
||||
}
|
||||
|
||||
if (leftSession.isArchived !== rightSession.isArchived) {
|
||||
return leftSession.isArchived ? 1 : -1;
|
||||
}
|
||||
|
||||
if (left.score !== right.score) {
|
||||
return right.score - left.score;
|
||||
}
|
||||
|
||||
return rightSession.updatedAt.localeCompare(leftSession.updatedAt);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user