mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 01:12:46 +00:00
refactor: setup sidebar workspace and session list
This commit is contained in:
@@ -51,6 +51,17 @@ export async function processCodexSessions() {
|
||||
}
|
||||
}
|
||||
|
||||
function getPathNumberVariants(value: number): string[] {
|
||||
const unpadded = String(value);
|
||||
const padded = unpadded.padStart(2, '0');
|
||||
|
||||
if (unpadded === padded) {
|
||||
return [unpadded];
|
||||
}
|
||||
|
||||
return [unpadded, padded];
|
||||
}
|
||||
|
||||
function buildCodexDatePathParts(createdAt: string): Array<{ year: string; month: string; day: string }> {
|
||||
const parsedDate = new Date(createdAt);
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
@@ -59,25 +70,40 @@ function buildCodexDatePathParts(createdAt: string): Array<{ year: string; month
|
||||
|
||||
const localDate = {
|
||||
year: String(parsedDate.getFullYear()),
|
||||
month: String(parsedDate.getMonth() + 1),
|
||||
day: String(parsedDate.getDate()),
|
||||
month: parsedDate.getMonth() + 1,
|
||||
day: parsedDate.getDate(),
|
||||
};
|
||||
|
||||
const utcDate = {
|
||||
year: String(parsedDate.getUTCFullYear()),
|
||||
month: String(parsedDate.getUTCMonth() + 1),
|
||||
day: String(parsedDate.getUTCDate()),
|
||||
month: parsedDate.getUTCMonth() + 1,
|
||||
day: parsedDate.getUTCDate(),
|
||||
};
|
||||
|
||||
if (
|
||||
const rawDateParts =
|
||||
localDate.year === utcDate.year &&
|
||||
localDate.month === utcDate.month &&
|
||||
localDate.day === utcDate.day
|
||||
) {
|
||||
return [localDate];
|
||||
localDate.month === utcDate.month &&
|
||||
localDate.day === utcDate.day
|
||||
? [localDate]
|
||||
: [localDate, utcDate];
|
||||
|
||||
const uniqueDateParts = new Map<string, { year: string; month: string; day: string }>();
|
||||
for (const datePart of rawDateParts) {
|
||||
const monthVariants = getPathNumberVariants(datePart.month);
|
||||
const dayVariants = getPathNumberVariants(datePart.day);
|
||||
|
||||
for (const month of monthVariants) {
|
||||
for (const day of dayVariants) {
|
||||
uniqueDateParts.set(`${datePart.year}-${month}-${day}`, {
|
||||
year: datePart.year,
|
||||
month,
|
||||
day,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [localDate, utcDate];
|
||||
return [...uniqueDateParts.values()];
|
||||
}
|
||||
|
||||
async function removeFileIfExists(filePath: string): Promise<boolean> {
|
||||
|
||||
171
server/src/modules/sidebar/sidebar.routes.ts
Normal file
171
server/src/modules/sidebar/sidebar.routes.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import express, { type Request, type Response } from 'express';
|
||||
|
||||
import { authenticateToken } from '@/modules/auth/auth.middleware.js';
|
||||
import {
|
||||
deleteSessionById,
|
||||
deleteWorkspaceByPath,
|
||||
getWorkspaceSessionsCollection,
|
||||
updateSessionNameById,
|
||||
updateWorkspaceNameByPath,
|
||||
updateWorkspaceStarByPath,
|
||||
} from '@/modules/sidebar/sidebar.service.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const getTrimmedString = (value: unknown): string => {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
};
|
||||
|
||||
const getWorkspacePathFromBody = (req: Request): string => getTrimmedString(req.body?.workspacePath);
|
||||
|
||||
router.get(
|
||||
'/api/sidebar/get-workspaces-sessions',
|
||||
authenticateToken,
|
||||
async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const workspaces = getWorkspaceSessionsCollection();
|
||||
res.json({ workspaces });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch workspaces';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/api/sidebar/update-workspace-star',
|
||||
authenticateToken,
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const workspacePath = getWorkspacePathFromBody(req);
|
||||
if (!workspacePath) {
|
||||
res.status(400).json({ error: 'workspacePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const isStarred = updateWorkspaceStarByPath(workspacePath);
|
||||
res.json({ success: true, workspacePath, isStarred });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update workspace star';
|
||||
const statusCode = message.toLowerCase().includes('not found') ? 404 : 500;
|
||||
res.status(statusCode).json({ error: message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/api/sidebar/update-workspace-custom-name',
|
||||
authenticateToken,
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const workspacePath = getWorkspacePathFromBody(req);
|
||||
if (!workspacePath) {
|
||||
res.status(400).json({ error: 'workspacePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const customWorkspaceName = getTrimmedString(req.body?.workspaceCustomName);
|
||||
updateWorkspaceNameByPath(workspacePath, customWorkspaceName || null);
|
||||
|
||||
res.json({ success: true, workspacePath, workspaceCustomName: customWorkspaceName || null });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update workspace name';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/api/sidebar/update-session-custom-name',
|
||||
authenticateToken,
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const sessionId = getTrimmedString(req.body?.sessionId);
|
||||
const sessionCustomName = getTrimmedString(req.body?.sessionCustomName);
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionCustomName) {
|
||||
res.status(400).json({ error: 'sessionCustomName is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionCustomName.length > 500) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ error: 'sessionCustomName must not exceed 500 characters' });
|
||||
return;
|
||||
}
|
||||
|
||||
updateSessionNameById(sessionId, sessionCustomName);
|
||||
res.json({ success: true, sessionId, sessionCustomName });
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Failed to update session name';
|
||||
const statusCode = message.toLowerCase().includes('not found') ? 404 : 500;
|
||||
res.status(statusCode).json({ error: message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/api/sidebar/delete-workspace',
|
||||
authenticateToken,
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const workspacePath = getWorkspacePathFromBody(req);
|
||||
if (!workspacePath) {
|
||||
res.status(400).json({ error: 'workspacePath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteWorkspaceByPath(workspacePath);
|
||||
res.json({
|
||||
success: true,
|
||||
workspacePath,
|
||||
...result,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete workspace';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/api/sidebar/delete-session',
|
||||
authenticateToken,
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const sessionId = getTrimmedString(req.body?.sessionId);
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteSessionById(sessionId);
|
||||
if (!result.deleted) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sessionId,
|
||||
...result,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete session';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
247
server/src/modules/sidebar/sidebar.service.ts
Normal file
247
server/src/modules/sidebar/sidebar.service.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { deleteClaudeSession } from '@/modules/providers/claude/claude.session-processor.js';
|
||||
import { deleteCodexSession } from '@/modules/providers/codex/codex.session-processor.js';
|
||||
import { deleteCursorSession } from '@/modules/providers/cursor/cursor.session-processor.js';
|
||||
import { deleteGeminiSession } from '@/modules/providers/gemini/gemini.session-processor.js';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
|
||||
import type { SessionsRow } from '@/shared/database/types.js';
|
||||
|
||||
export type SidebarSessionRecord = {
|
||||
sessionId: string;
|
||||
id: string;
|
||||
provider: SessionsRow['provider'];
|
||||
customName: string | null;
|
||||
summary: string;
|
||||
workspacePath: string;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
lastActivity: string | null;
|
||||
};
|
||||
|
||||
export type SidebarWorkspaceRecord = {
|
||||
workspaceOriginalPath: string;
|
||||
workspaceCustomName: string | null;
|
||||
workspaceDisplayName: string;
|
||||
isStarred: boolean;
|
||||
lastActivity: string | null;
|
||||
sessions: SidebarSessionRecord[];
|
||||
};
|
||||
|
||||
export type DeleteSessionResult = {
|
||||
deleted: boolean;
|
||||
jsonlDeleted: boolean;
|
||||
};
|
||||
|
||||
export type DeleteWorkspaceResult = {
|
||||
deletedWorkspace: boolean;
|
||||
deletedSessionCount: number;
|
||||
jsonlDeletedCount: number;
|
||||
failedSessionFileDeletes: string[];
|
||||
};
|
||||
|
||||
type SessionDeletionTarget = Pick<SessionsRow, 'session_id' | 'provider' | 'workspace_path' | 'created_at'>;
|
||||
|
||||
const parseTimestamp = (timestamp: string | null | undefined): number => {
|
||||
if (!timestamp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// SQLite CURRENT_TIMESTAMP is UTC but stored without timezone ("YYYY-MM-DD HH:MM:SS").
|
||||
// Normalize this format so parsing is always timezone-correct.
|
||||
const sqliteUtcPattern = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
||||
const normalizedTimestamp = sqliteUtcPattern.test(timestamp)
|
||||
? `${timestamp.replace(' ', 'T')}Z`
|
||||
: timestamp;
|
||||
|
||||
const parsed = new Date(normalizedTimestamp).getTime();
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
};
|
||||
|
||||
const toSidebarSessionRecord = (session: SessionsRow): SidebarSessionRecord => {
|
||||
const lastActivity = session.updated_at || session.created_at || null;
|
||||
|
||||
return {
|
||||
sessionId: session.session_id,
|
||||
id: session.session_id,
|
||||
provider: session.provider,
|
||||
customName: session.custom_name,
|
||||
summary: session.custom_name || 'Untitled Session',
|
||||
workspacePath: session.workspace_path,
|
||||
createdAt: session.created_at || null,
|
||||
updatedAt: session.updated_at || null,
|
||||
lastActivity,
|
||||
};
|
||||
};
|
||||
|
||||
const sortSessionsByLastActivity = (sessions: SidebarSessionRecord[]): SidebarSessionRecord[] =>
|
||||
[...sessions].sort((left, right) => {
|
||||
const timestampDifference =
|
||||
parseTimestamp(right.lastActivity) - parseTimestamp(left.lastActivity);
|
||||
|
||||
if (timestampDifference !== 0) {
|
||||
return timestampDifference;
|
||||
}
|
||||
|
||||
return right.sessionId.localeCompare(left.sessionId);
|
||||
});
|
||||
|
||||
const sortWorkspacesByLastActivity = (
|
||||
workspaces: SidebarWorkspaceRecord[],
|
||||
): SidebarWorkspaceRecord[] =>
|
||||
[...workspaces].sort((left, right) => {
|
||||
const timestampDifference =
|
||||
parseTimestamp(right.lastActivity) - parseTimestamp(left.lastActivity);
|
||||
|
||||
if (timestampDifference !== 0) {
|
||||
return timestampDifference;
|
||||
}
|
||||
|
||||
return left.workspaceDisplayName.localeCompare(right.workspaceDisplayName);
|
||||
});
|
||||
|
||||
const deleteSessionFileByProvider = async (
|
||||
session: SessionDeletionTarget,
|
||||
): Promise<boolean> => {
|
||||
switch (session.provider) {
|
||||
case 'claude':
|
||||
return deleteClaudeSession(session.session_id, session.workspace_path);
|
||||
case 'codex':
|
||||
return deleteCodexSession(session.session_id, session.created_at);
|
||||
case 'cursor':
|
||||
return deleteCursorSession(session.session_id, session.workspace_path);
|
||||
case 'gemini':
|
||||
return deleteGeminiSession(session.session_id);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkspaceSessionsCollection = (): SidebarWorkspaceRecord[] => {
|
||||
const workspaceRows = workspaceOriginalPathsDb.getWorkspacePaths();
|
||||
const sessionRows = sessionsDb.getAllSessions();
|
||||
const sessionsByWorkspace = new Map<string, SidebarSessionRecord[]>();
|
||||
|
||||
// Build grouped sessions once to keep the response shape deterministic.
|
||||
for (const sessionRow of sessionRows) {
|
||||
const existing = sessionsByWorkspace.get(sessionRow.workspace_path) || [];
|
||||
existing.push(toSidebarSessionRecord(sessionRow));
|
||||
sessionsByWorkspace.set(sessionRow.workspace_path, existing);
|
||||
}
|
||||
|
||||
const workspaceRecords = workspaceRows.map((workspaceRow) => {
|
||||
const sessions = sortSessionsByLastActivity(
|
||||
sessionsByWorkspace.get(workspaceRow.workspace_path) || [],
|
||||
);
|
||||
const lastActivity = sessions[0]?.lastActivity || null;
|
||||
|
||||
return {
|
||||
workspaceOriginalPath: workspaceRow.workspace_path,
|
||||
workspaceCustomName: workspaceRow.custom_workspace_name,
|
||||
workspaceDisplayName:
|
||||
workspaceRow.custom_workspace_name ||
|
||||
path.basename(workspaceRow.workspace_path) ||
|
||||
workspaceRow.workspace_path,
|
||||
isStarred: workspaceRow.isStarred === 1,
|
||||
lastActivity,
|
||||
sessions,
|
||||
};
|
||||
});
|
||||
|
||||
return sortWorkspacesByLastActivity(workspaceRecords);
|
||||
};
|
||||
|
||||
export const updateWorkspaceStarByPath = (workspacePath: string): boolean => {
|
||||
const workspaceRow = workspaceOriginalPathsDb.getWorkspacePath(workspacePath);
|
||||
if (!workspaceRow) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
|
||||
const nextIsStarred = workspaceRow.isStarred !== 1;
|
||||
workspaceOriginalPathsDb.updateWorkspaceIsStarred(workspacePath, nextIsStarred);
|
||||
|
||||
return nextIsStarred;
|
||||
};
|
||||
|
||||
export const updateWorkspaceNameByPath = (
|
||||
workspacePath: string,
|
||||
workspaceCustomName: string | null,
|
||||
): void => {
|
||||
workspaceOriginalPathsDb.updateCustomWorkspaceName(workspacePath, workspaceCustomName);
|
||||
};
|
||||
|
||||
export const updateSessionNameById = (
|
||||
sessionId: string,
|
||||
sessionCustomName: string,
|
||||
): void => {
|
||||
const sessionMetadata = sessionsDb.getSessionById(sessionId);
|
||||
if (!sessionMetadata) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
|
||||
sessionsDb.updateSessionCustomName(sessionId, sessionCustomName);
|
||||
};
|
||||
|
||||
export const deleteSessionById = async (
|
||||
sessionId: string,
|
||||
): Promise<DeleteSessionResult> => {
|
||||
const sessionMetadata = sessionsDb.getSessionById(sessionId);
|
||||
if (!sessionMetadata) {
|
||||
return {
|
||||
deleted: false,
|
||||
jsonlDeleted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const jsonlDeleted = await deleteSessionFileByProvider({
|
||||
session_id: sessionMetadata.session_id,
|
||||
provider: sessionMetadata.provider,
|
||||
workspace_path: sessionMetadata.workspace_path,
|
||||
created_at: sessionMetadata.created_at,
|
||||
});
|
||||
|
||||
sessionsDb.deleteSession(sessionId);
|
||||
|
||||
return {
|
||||
deleted: true,
|
||||
jsonlDeleted,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteWorkspaceByPath = async (
|
||||
workspacePath: string,
|
||||
): Promise<DeleteWorkspaceResult> => {
|
||||
const sessionRows = sessionsDb.getSessionsByWorkspacePath(workspacePath);
|
||||
const failedSessionFileDeletes: string[] = [];
|
||||
let jsonlDeletedCount = 0;
|
||||
|
||||
// Remove all session files first, then clean up DB rows.
|
||||
for (const sessionRow of sessionRows) {
|
||||
try {
|
||||
const deleted = await deleteSessionFileByProvider({
|
||||
session_id: sessionRow.session_id,
|
||||
provider: sessionRow.provider,
|
||||
workspace_path: sessionRow.workspace_path,
|
||||
created_at: sessionRow.created_at,
|
||||
});
|
||||
|
||||
if (deleted) {
|
||||
jsonlDeletedCount += 1;
|
||||
}
|
||||
} catch {
|
||||
failedSessionFileDeletes.push(sessionRow.session_id);
|
||||
} finally {
|
||||
sessionsDb.deleteSession(sessionRow.session_id);
|
||||
}
|
||||
}
|
||||
|
||||
workspaceOriginalPathsDb.deleteWorkspacePath(workspacePath);
|
||||
|
||||
return {
|
||||
deletedWorkspace: true,
|
||||
deletedSessionCount: sessionRows.length,
|
||||
jsonlDeletedCount,
|
||||
failedSessionFileDeletes,
|
||||
};
|
||||
};
|
||||
@@ -61,6 +61,7 @@ const [
|
||||
geminiRoutes,
|
||||
pluginsRoutes,
|
||||
messagesRoutes,
|
||||
sidebarRoutes,
|
||||
projectsInlineRoutes,
|
||||
filesRoutes,
|
||||
sessionsInlineRoutes,
|
||||
@@ -85,6 +86,7 @@ const [
|
||||
importRoute('./modules/gemini/gemini.routes.js'),
|
||||
importRoute('./modules/plugins/plugins.routes.js'),
|
||||
importRoute('./modules/messages/messages.routes.js'),
|
||||
importRoute('./modules/sidebar/sidebar.routes.js'),
|
||||
importRoute('./modules/projects/projects.inline.routes.js'),
|
||||
importRoute('./modules/files/files.routes.js'),
|
||||
importRoute('./modules/sessions/sessions.inline.routes.js'),
|
||||
@@ -174,6 +176,9 @@ app.use('/api/plugins', authenticateToken, pluginsRoutes);
|
||||
// Unified session messages route (protected)
|
||||
app.use('/api/sessions', authenticateToken, messagesRoutes);
|
||||
|
||||
// Refactored sidebar routes (protected)
|
||||
app.use(sidebarRoutes);
|
||||
|
||||
// Agent API Routes (uses API key authentication)
|
||||
app.use('/api/agent', agentRoutes);
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ export const runMigrations = (db: Database) => {
|
||||
db.exec(
|
||||
"CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id)"
|
||||
);
|
||||
db.exec(
|
||||
"CREATE INDEX IF NOT EXISTS idx_sessions_workspace_path ON sessions(workspace_path)"
|
||||
);
|
||||
const sessionsTableInfo = db.prepare("PRAGMA table_info(sessions)").all() as { name: string }[];
|
||||
const sessionColumnNames = sessionsTableInfo.map((col) => col.name);
|
||||
addColumnToTableIfNotExists(db, "sessions", sessionColumnNames, "created_at", "DATETIME");
|
||||
@@ -66,6 +69,16 @@ export const runMigrations = (db: Database) => {
|
||||
"custom_workspace_name",
|
||||
"TEXT DEFAULT NULL",
|
||||
);
|
||||
addColumnToTableIfNotExists(
|
||||
db,
|
||||
"workspace_original_paths",
|
||||
workspaceOriginalPathsColumnNames,
|
||||
"isStarred",
|
||||
"BOOLEAN DEFAULT 0",
|
||||
);
|
||||
db.exec(
|
||||
"CREATE INDEX IF NOT EXISTS idx_workspace_original_paths_is_starred ON workspace_original_paths(isStarred)"
|
||||
);
|
||||
|
||||
db.exec(LAST_SCANNED_AT_SQL);
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export const sessionsDb = {
|
||||
const db = getConnection();
|
||||
db.prepare(
|
||||
`UPDATE sessions
|
||||
SET custom_name = ?, updated_at = CURRENT_TIMESTAMP
|
||||
SET custom_name = ?
|
||||
WHERE session_id = ?`
|
||||
).run(customName, sessionId);
|
||||
},
|
||||
@@ -100,7 +100,7 @@ export const sessionsDb = {
|
||||
const db = getConnection();
|
||||
db.prepare(
|
||||
`UPDATE sessions
|
||||
SET custom_name = ?, updated_at = CURRENT_TIMESTAMP
|
||||
SET custom_name = ?
|
||||
WHERE session_id = ? AND provider = ?`
|
||||
).run(customName, sessionId, provider);
|
||||
},
|
||||
@@ -118,6 +118,27 @@ export const sessionsDb = {
|
||||
return row ?? null;
|
||||
},
|
||||
|
||||
getAllSessions(): SessionsRow[] {
|
||||
const db = getConnection();
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT session_id, provider, workspace_path, custom_name, created_at, updated_at
|
||||
FROM sessions`
|
||||
)
|
||||
.all() as SessionsRow[];
|
||||
},
|
||||
|
||||
getSessionsByWorkspacePath(workspacePath: string): SessionsRow[] {
|
||||
const db = getConnection();
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT session_id, provider, workspace_path, custom_name, created_at, updated_at
|
||||
FROM sessions
|
||||
WHERE workspace_path = ?`
|
||||
)
|
||||
.all(workspacePath) as SessionsRow[];
|
||||
},
|
||||
|
||||
getSessionName(sessionId: string, provider: string): string | null {
|
||||
const db = getConnection();
|
||||
const row = db
|
||||
|
||||
@@ -16,6 +16,25 @@ export const workspaceOriginalPathsDb = {
|
||||
`).run(workspacePath, customWorkspaceName);
|
||||
},
|
||||
|
||||
getWorkspacePath(workspacePath: string): WorkspaceOriginalPathRow | null {
|
||||
const db = getConnection();
|
||||
const row = db.prepare(`
|
||||
SELECT workspace_path, custom_workspace_name, isStarred
|
||||
FROM workspace_original_paths
|
||||
WHERE workspace_path = ?
|
||||
`).get(workspacePath) as WorkspaceOriginalPathRow | undefined;
|
||||
|
||||
return row ?? null;
|
||||
},
|
||||
|
||||
getWorkspacePaths(): WorkspaceOriginalPathRow[] {
|
||||
const db = getConnection();
|
||||
return db.prepare(`
|
||||
SELECT workspace_path, custom_workspace_name, isStarred
|
||||
FROM workspace_original_paths
|
||||
`).all() as WorkspaceOriginalPathRow[];
|
||||
},
|
||||
|
||||
getCustomWorkspaceName(workspacePath: string): string | null {
|
||||
const db = getConnection();
|
||||
const row = db.prepare(`
|
||||
@@ -35,4 +54,21 @@ export const workspaceOriginalPathsDb = {
|
||||
ON CONFLICT(workspace_path) DO UPDATE SET custom_workspace_name = excluded.custom_workspace_name
|
||||
`).run(workspacePath, customWorkspaceName);
|
||||
},
|
||||
|
||||
updateWorkspaceIsStarred(workspacePath: string, isStarred: boolean): void {
|
||||
const db = getConnection();
|
||||
db.prepare(`
|
||||
UPDATE workspace_original_paths
|
||||
SET isStarred = ?
|
||||
WHERE workspace_path = ?
|
||||
`).run(isStarred ? 1 : 0, workspacePath);
|
||||
},
|
||||
|
||||
deleteWorkspacePath(workspacePath: string): void {
|
||||
const db = getConnection();
|
||||
db.prepare(`
|
||||
DELETE FROM workspace_original_paths
|
||||
WHERE workspace_path = ?
|
||||
`).run(workspacePath);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -86,7 +86,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
||||
export const WORK_SPACE_PATH_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS workspace_original_paths (
|
||||
workspace_path TEXT PRIMARY KEY NOT NULL,
|
||||
custom_workspace_name TEXT DEFAULT NULL
|
||||
custom_workspace_name TEXT DEFAULT NULL,
|
||||
isStarred BOOLEAN DEFAULT 0
|
||||
);
|
||||
`
|
||||
|
||||
@@ -135,8 +136,10 @@ CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(
|
||||
|
||||
${SESSIONS_TABLE_SCHEMA_SQL}
|
||||
CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_workspace_path ON sessions(workspace_path);
|
||||
|
||||
${WORK_SPACE_PATH_SQL}
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_original_paths_is_starred ON workspace_original_paths(isStarred);
|
||||
|
||||
${LAST_SCANNED_AT_SQL}
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ export type SessionWithSummary = {
|
||||
export type WorkspaceOriginalPathRow = {
|
||||
workspace_path: string;
|
||||
custom_workspace_name: string | null;
|
||||
isStarred: number; // SQLite boolean: 0 | 1
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user