mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-04 04:13:03 +08:00
feat: use workspace ids instead of paths for workspace operations
This commit is contained in:
@@ -16,17 +16,17 @@ const getTrimmedString = (value: unknown): string => {
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseWorkspacePathFromBody = (req: Request): string => {
|
const parseWorkspaceIdFromBody = (req: Request): string => {
|
||||||
const body = req.body as Record<string, unknown> | undefined;
|
const body = req.body as Record<string, unknown> | undefined;
|
||||||
const workspacePath = getTrimmedString(body?.workspacePath);
|
const workspaceId = getTrimmedString(body?.workspaceId);
|
||||||
if (!workspacePath) {
|
if (!workspaceId) {
|
||||||
throw new AppError('workspacePath is required.', {
|
throw new AppError('workspaceId is required.', {
|
||||||
code: 'WORKSPACE_PATH_REQUIRED',
|
code: 'WORKSPACE_ID_REQUIRED',
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspacePath;
|
return workspaceId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseWorkspaceCustomNameFromBody = (req: Request): string | null => {
|
const parseWorkspaceCustomNameFromBody = (req: Request): string | null => {
|
||||||
@@ -46,27 +46,27 @@ router.get(
|
|||||||
router.patch(
|
router.patch(
|
||||||
'/star',
|
'/star',
|
||||||
asyncHandler(async (req: Request, res: Response) => {
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
const workspacePath = parseWorkspacePathFromBody(req);
|
const workspaceId = parseWorkspaceIdFromBody(req);
|
||||||
const isStarred = workspaceService.toggleWorkspaceStar(workspacePath);
|
const isStarred = workspaceService.toggleWorkspaceStar(workspaceId);
|
||||||
res.json(createApiSuccessResponse({ workspacePath, isStarred }));
|
res.json(createApiSuccessResponse({ workspaceId, isStarred }));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/name',
|
'/name',
|
||||||
asyncHandler(async (req: Request, res: Response) => {
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
const workspacePath = parseWorkspacePathFromBody(req);
|
const workspaceId = parseWorkspaceIdFromBody(req);
|
||||||
const workspaceCustomName = parseWorkspaceCustomNameFromBody(req);
|
const workspaceCustomName = parseWorkspaceCustomNameFromBody(req);
|
||||||
workspaceService.updateWorkspaceCustomName(workspacePath, workspaceCustomName);
|
workspaceService.updateWorkspaceCustomName(workspaceId, workspaceCustomName);
|
||||||
res.json(createApiSuccessResponse({ workspacePath, workspaceCustomName }));
|
res.json(createApiSuccessResponse({ workspaceId, workspaceCustomName }));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/',
|
'/',
|
||||||
asyncHandler(async (req: Request, res: Response) => {
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
const workspacePath = parseWorkspacePathFromBody(req);
|
const workspaceId = parseWorkspaceIdFromBody(req);
|
||||||
const result = await workspaceService.deleteWorkspace(workspacePath);
|
const result = await workspaceService.deleteWorkspace(workspaceId);
|
||||||
res.json(createApiSuccessResponse(result));
|
res.json(createApiSuccessResponse(result));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type WorkspaceSessionRecord = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceRecord = {
|
export type WorkspaceRecord = {
|
||||||
|
workspaceId: string;
|
||||||
workspaceOriginalPath: string;
|
workspaceOriginalPath: string;
|
||||||
workspaceCustomName: string | null;
|
workspaceCustomName: string | null;
|
||||||
workspaceDisplayName: string;
|
workspaceDisplayName: string;
|
||||||
@@ -107,6 +108,7 @@ const buildWorkspaceSessionCollection = (): WorkspaceRecord[] => {
|
|||||||
const lastActivity = sessions[0]?.lastActivity || null;
|
const lastActivity = sessions[0]?.lastActivity || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
workspaceId: workspaceRow.workspace_id,
|
||||||
workspaceOriginalPath: workspaceRow.workspace_path,
|
workspaceOriginalPath: workspaceRow.workspace_path,
|
||||||
workspaceCustomName: workspaceRow.custom_workspace_name,
|
workspaceCustomName: workspaceRow.custom_workspace_name,
|
||||||
workspaceDisplayName:
|
workspaceDisplayName:
|
||||||
@@ -130,8 +132,8 @@ export const workspaceService = {
|
|||||||
return buildWorkspaceSessionCollection();
|
return buildWorkspaceSessionCollection();
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleWorkspaceStar(workspacePath: string): boolean {
|
toggleWorkspaceStar(workspaceId: string): boolean {
|
||||||
const workspaceRow = workspaceOriginalPathsDb.getWorkspacePath(workspacePath);
|
const workspaceRow = workspaceOriginalPathsDb.getWorkspaceById(workspaceId);
|
||||||
if (!workspaceRow) {
|
if (!workspaceRow) {
|
||||||
throw new AppError('Workspace not found.', {
|
throw new AppError('Workspace not found.', {
|
||||||
code: 'WORKSPACE_NOT_FOUND',
|
code: 'WORKSPACE_NOT_FOUND',
|
||||||
@@ -140,22 +142,40 @@ export const workspaceService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextIsStarred = workspaceRow.isStarred !== 1;
|
const nextIsStarred = workspaceRow.isStarred !== 1;
|
||||||
workspaceOriginalPathsDb.updateWorkspaceIsStarred(workspacePath, nextIsStarred);
|
workspaceOriginalPathsDb.updateWorkspaceIsStarredById(workspaceId, nextIsStarred);
|
||||||
|
|
||||||
return nextIsStarred;
|
return nextIsStarred;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateWorkspaceCustomName(workspacePath: string, workspaceCustomName: string | null): void {
|
updateWorkspaceCustomName(workspaceId: string, workspaceCustomName: string | null): void {
|
||||||
workspaceOriginalPathsDb.updateCustomWorkspaceName(workspacePath, workspaceCustomName);
|
const workspaceRow = workspaceOriginalPathsDb.getWorkspaceById(workspaceId);
|
||||||
|
if (!workspaceRow) {
|
||||||
|
throw new AppError('Workspace not found.', {
|
||||||
|
code: 'WORKSPACE_NOT_FOUND',
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceOriginalPathsDb.updateCustomWorkspaceNameById(workspaceId, workspaceCustomName);
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteWorkspace(workspacePath: string): Promise<{
|
async deleteWorkspace(workspaceId: string): Promise<{
|
||||||
|
workspaceId: string;
|
||||||
workspacePath: string;
|
workspacePath: string;
|
||||||
deletedWorkspace: boolean;
|
deletedWorkspace: boolean;
|
||||||
deletedSessionCount: number;
|
deletedSessionCount: number;
|
||||||
jsonlDeletedCount: number;
|
jsonlDeletedCount: number;
|
||||||
failedSessionFileDeletes: string[];
|
failedSessionFileDeletes: string[];
|
||||||
}> {
|
}> {
|
||||||
|
const workspaceRow = workspaceOriginalPathsDb.getWorkspaceById(workspaceId);
|
||||||
|
if (!workspaceRow) {
|
||||||
|
throw new AppError('Workspace not found.', {
|
||||||
|
code: 'WORKSPACE_NOT_FOUND',
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspacePath = workspaceRow.workspace_path;
|
||||||
const sessionRows = sessionsDb.getSessionsByWorkspacePath(workspacePath);
|
const sessionRows = sessionsDb.getSessionsByWorkspacePath(workspacePath);
|
||||||
const failedSessionFileDeletes: string[] = [];
|
const failedSessionFileDeletes: string[] = [];
|
||||||
let jsonlDeletedCount = 0;
|
let jsonlDeletedCount = 0;
|
||||||
@@ -175,9 +195,10 @@ export const workspaceService = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaceOriginalPathsDb.deleteWorkspacePath(workspacePath);
|
workspaceOriginalPathsDb.deleteWorkspaceById(workspaceId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
workspaceId,
|
||||||
workspacePath,
|
workspacePath,
|
||||||
deletedWorkspace: true,
|
deletedWorkspace: true,
|
||||||
deletedSessionCount: sessionRows.length,
|
deletedSessionCount: sessionRows.length,
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import {
|
|||||||
} from "@/shared/database/schema.js";
|
} from "@/shared/database/schema.js";
|
||||||
import { logger } from "@/shared/utils/logger.js";
|
import { logger } from "@/shared/utils/logger.js";
|
||||||
|
|
||||||
|
const SQLITE_UUID_SQL = `
|
||||||
|
lower(hex(randomblob(4))) || '-' ||
|
||||||
|
lower(hex(randomblob(2))) || '-' ||
|
||||||
|
lower(hex(randomblob(2))) || '-' ||
|
||||||
|
lower(hex(randomblob(2))) || '-' ||
|
||||||
|
lower(hex(randomblob(6)))
|
||||||
|
`;
|
||||||
|
|
||||||
const addColumnToTableIfNotExists = (
|
const addColumnToTableIfNotExists = (
|
||||||
db: Database,
|
db: Database,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
@@ -61,7 +69,9 @@ export const runMigrations = (db: Database) => {
|
|||||||
db.exec("UPDATE sessions SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)");
|
db.exec("UPDATE sessions SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)");
|
||||||
|
|
||||||
db.exec(WORK_SPACE_PATH_SQL);
|
db.exec(WORK_SPACE_PATH_SQL);
|
||||||
const workspaceOriginalPathsTableInfo = db.prepare("PRAGMA table_info(workspace_original_paths)").all() as { name: string }[];
|
const workspaceOriginalPathsTableInfo = db
|
||||||
|
.prepare("PRAGMA table_info(workspace_original_paths)")
|
||||||
|
.all() as { name: string; pk: number }[];
|
||||||
const workspaceOriginalPathsColumnNames = workspaceOriginalPathsTableInfo.map((col) => col.name);
|
const workspaceOriginalPathsColumnNames = workspaceOriginalPathsTableInfo.map((col) => col.name);
|
||||||
addColumnToTableIfNotExists(
|
addColumnToTableIfNotExists(
|
||||||
db,
|
db,
|
||||||
@@ -70,6 +80,13 @@ export const runMigrations = (db: Database) => {
|
|||||||
"custom_workspace_name",
|
"custom_workspace_name",
|
||||||
"TEXT DEFAULT NULL",
|
"TEXT DEFAULT NULL",
|
||||||
);
|
);
|
||||||
|
addColumnToTableIfNotExists(
|
||||||
|
db,
|
||||||
|
"workspace_original_paths",
|
||||||
|
workspaceOriginalPathsColumnNames,
|
||||||
|
"workspace_id",
|
||||||
|
"TEXT",
|
||||||
|
);
|
||||||
addColumnToTableIfNotExists(
|
addColumnToTableIfNotExists(
|
||||||
db,
|
db,
|
||||||
"workspace_original_paths",
|
"workspace_original_paths",
|
||||||
@@ -77,9 +94,61 @@ export const runMigrations = (db: Database) => {
|
|||||||
"isStarred",
|
"isStarred",
|
||||||
"BOOLEAN DEFAULT 0",
|
"BOOLEAN DEFAULT 0",
|
||||||
);
|
);
|
||||||
|
db.exec(`
|
||||||
|
UPDATE workspace_original_paths
|
||||||
|
SET workspace_id = ${SQLITE_UUID_SQL}
|
||||||
|
WHERE workspace_id IS NULL OR trim(workspace_id) = ''
|
||||||
|
`);
|
||||||
|
const workspaceOriginalPathsPrimaryKeyColumn =
|
||||||
|
workspaceOriginalPathsTableInfo.find((column) => column.pk === 1)?.name ?? null;
|
||||||
|
if (workspaceOriginalPathsPrimaryKeyColumn !== "workspace_id") {
|
||||||
|
logger.info(
|
||||||
|
"Running migration: Rebuilding workspace_original_paths to set workspace_id as primary key",
|
||||||
|
);
|
||||||
|
db.exec("PRAGMA foreign_keys = OFF");
|
||||||
|
try {
|
||||||
|
db.exec("BEGIN TRANSACTION");
|
||||||
|
db.exec("DROP TABLE IF EXISTS workspace_original_paths__new");
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE workspace_original_paths__new (
|
||||||
|
workspace_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
workspace_path TEXT NOT NULL UNIQUE,
|
||||||
|
custom_workspace_name TEXT DEFAULT NULL,
|
||||||
|
isStarred BOOLEAN DEFAULT 0
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
db.exec(`
|
||||||
|
INSERT INTO workspace_original_paths__new (
|
||||||
|
workspace_id,
|
||||||
|
workspace_path,
|
||||||
|
custom_workspace_name,
|
||||||
|
isStarred
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN workspace_id IS NULL OR trim(workspace_id) = ''
|
||||||
|
THEN ${SQLITE_UUID_SQL}
|
||||||
|
ELSE workspace_id
|
||||||
|
END,
|
||||||
|
workspace_path,
|
||||||
|
custom_workspace_name,
|
||||||
|
COALESCE(isStarred, 0)
|
||||||
|
FROM workspace_original_paths
|
||||||
|
`);
|
||||||
|
db.exec("DROP TABLE workspace_original_paths");
|
||||||
|
db.exec("ALTER TABLE workspace_original_paths__new RENAME TO workspace_original_paths");
|
||||||
|
db.exec("COMMIT");
|
||||||
|
} catch (migrationError) {
|
||||||
|
db.exec("ROLLBACK");
|
||||||
|
throw migrationError;
|
||||||
|
} finally {
|
||||||
|
db.exec("PRAGMA foreign_keys = ON");
|
||||||
|
}
|
||||||
|
}
|
||||||
db.exec(
|
db.exec(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_workspace_original_paths_is_starred ON workspace_original_paths(isStarred)"
|
"CREATE INDEX IF NOT EXISTS idx_workspace_original_paths_is_starred ON workspace_original_paths(isStarred)"
|
||||||
);
|
);
|
||||||
|
db.exec("DROP INDEX IF EXISTS idx_workspace_original_paths_workspace_id");
|
||||||
|
|
||||||
db.exec(LAST_SCANNED_AT_SQL);
|
db.exec(LAST_SCANNED_AT_SQL);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import { getConnection } from '@/shared/database/connection.js';
|
import { getConnection } from '@/shared/database/connection.js';
|
||||||
import type { WorkspaceOriginalPathRow } from '@/shared/database/types.js';
|
import type { WorkspaceOriginalPathRow } from '@/shared/database/types.js';
|
||||||
|
|
||||||
@@ -5,21 +7,21 @@ export const workspaceOriginalPathsDb = {
|
|||||||
createWorkspacePath(workspacePath: string, customWorkspaceName: string | null = null): void {
|
createWorkspacePath(workspacePath: string, customWorkspaceName: string | null = null): void {
|
||||||
const db = getConnection();
|
const db = getConnection();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO workspace_original_paths (workspace_path, custom_workspace_name)
|
INSERT INTO workspace_original_paths (workspace_id, workspace_path, custom_workspace_name)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON CONFLICT(workspace_path) DO UPDATE SET
|
ON CONFLICT(workspace_path) DO UPDATE SET
|
||||||
custom_workspace_name = CASE
|
custom_workspace_name = CASE
|
||||||
WHEN workspace_original_paths.custom_workspace_name IS NULL OR workspace_original_paths.custom_workspace_name = ''
|
WHEN workspace_original_paths.custom_workspace_name IS NULL OR workspace_original_paths.custom_workspace_name = ''
|
||||||
THEN excluded.custom_workspace_name
|
THEN excluded.custom_workspace_name
|
||||||
ELSE workspace_original_paths.custom_workspace_name
|
ELSE workspace_original_paths.custom_workspace_name
|
||||||
END
|
END
|
||||||
`).run(workspacePath, customWorkspaceName);
|
`).run(randomUUID(), workspacePath, customWorkspaceName);
|
||||||
},
|
},
|
||||||
|
|
||||||
getWorkspacePath(workspacePath: string): WorkspaceOriginalPathRow | null {
|
getWorkspacePath(workspacePath: string): WorkspaceOriginalPathRow | null {
|
||||||
const db = getConnection();
|
const db = getConnection();
|
||||||
const row = db.prepare(`
|
const row = db.prepare(`
|
||||||
SELECT workspace_path, custom_workspace_name, isStarred
|
SELECT workspace_id, workspace_path, custom_workspace_name, isStarred
|
||||||
FROM workspace_original_paths
|
FROM workspace_original_paths
|
||||||
WHERE workspace_path = ?
|
WHERE workspace_path = ?
|
||||||
`).get(workspacePath) as WorkspaceOriginalPathRow | undefined;
|
`).get(workspacePath) as WorkspaceOriginalPathRow | undefined;
|
||||||
@@ -27,10 +29,21 @@ export const workspaceOriginalPathsDb = {
|
|||||||
return row ?? null;
|
return row ?? null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getWorkspaceById(workspaceId: string): WorkspaceOriginalPathRow | null {
|
||||||
|
const db = getConnection();
|
||||||
|
const row = db.prepare(`
|
||||||
|
SELECT workspace_id, workspace_path, custom_workspace_name, isStarred
|
||||||
|
FROM workspace_original_paths
|
||||||
|
WHERE workspace_id = ?
|
||||||
|
`).get(workspaceId) as WorkspaceOriginalPathRow | undefined;
|
||||||
|
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
getWorkspacePaths(): WorkspaceOriginalPathRow[] {
|
getWorkspacePaths(): WorkspaceOriginalPathRow[] {
|
||||||
const db = getConnection();
|
const db = getConnection();
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT workspace_path, custom_workspace_name, isStarred
|
SELECT workspace_id, workspace_path, custom_workspace_name, isStarred
|
||||||
FROM workspace_original_paths
|
FROM workspace_original_paths
|
||||||
`).all() as WorkspaceOriginalPathRow[];
|
`).all() as WorkspaceOriginalPathRow[];
|
||||||
},
|
},
|
||||||
@@ -49,10 +62,19 @@ export const workspaceOriginalPathsDb = {
|
|||||||
updateCustomWorkspaceName(workspacePath: string, customWorkspaceName: string | null): void {
|
updateCustomWorkspaceName(workspacePath: string, customWorkspaceName: string | null): void {
|
||||||
const db = getConnection();
|
const db = getConnection();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO workspace_original_paths (workspace_path, custom_workspace_name)
|
INSERT INTO workspace_original_paths (workspace_id, workspace_path, custom_workspace_name)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON CONFLICT(workspace_path) DO UPDATE SET custom_workspace_name = excluded.custom_workspace_name
|
ON CONFLICT(workspace_path) DO UPDATE SET custom_workspace_name = excluded.custom_workspace_name
|
||||||
`).run(workspacePath, customWorkspaceName);
|
`).run(randomUUID(), workspacePath, customWorkspaceName);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCustomWorkspaceNameById(workspaceId: string, customWorkspaceName: string | null): void {
|
||||||
|
const db = getConnection();
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE workspace_original_paths
|
||||||
|
SET custom_workspace_name = ?
|
||||||
|
WHERE workspace_id = ?
|
||||||
|
`).run(customWorkspaceName, workspaceId);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateWorkspaceIsStarred(workspacePath: string, isStarred: boolean): void {
|
updateWorkspaceIsStarred(workspacePath: string, isStarred: boolean): void {
|
||||||
@@ -64,6 +86,15 @@ export const workspaceOriginalPathsDb = {
|
|||||||
`).run(isStarred ? 1 : 0, workspacePath);
|
`).run(isStarred ? 1 : 0, workspacePath);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateWorkspaceIsStarredById(workspaceId: string, isStarred: boolean): void {
|
||||||
|
const db = getConnection();
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE workspace_original_paths
|
||||||
|
SET isStarred = ?
|
||||||
|
WHERE workspace_id = ?
|
||||||
|
`).run(isStarred ? 1 : 0, workspaceId);
|
||||||
|
},
|
||||||
|
|
||||||
deleteWorkspacePath(workspacePath: string): void {
|
deleteWorkspacePath(workspacePath: string): void {
|
||||||
const db = getConnection();
|
const db = getConnection();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
@@ -71,4 +102,12 @@ export const workspaceOriginalPathsDb = {
|
|||||||
WHERE workspace_path = ?
|
WHERE workspace_path = ?
|
||||||
`).run(workspacePath);
|
`).run(workspacePath);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteWorkspaceById(workspaceId: string): void {
|
||||||
|
const db = getConnection();
|
||||||
|
db.prepare(`
|
||||||
|
DELETE FROM workspace_original_paths
|
||||||
|
WHERE workspace_id = ?
|
||||||
|
`).run(workspaceId);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|||||||
|
|
||||||
export const WORK_SPACE_PATH_SQL = `
|
export const WORK_SPACE_PATH_SQL = `
|
||||||
CREATE TABLE IF NOT EXISTS workspace_original_paths (
|
CREATE TABLE IF NOT EXISTS workspace_original_paths (
|
||||||
workspace_path TEXT PRIMARY KEY NOT NULL,
|
workspace_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
workspace_path TEXT NOT NULL UNIQUE,
|
||||||
custom_workspace_name TEXT DEFAULT NULL,
|
custom_workspace_name TEXT DEFAULT NULL,
|
||||||
isStarred BOOLEAN DEFAULT 0
|
isStarred BOOLEAN DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export type SessionWithSummary = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceOriginalPathRow = {
|
export type WorkspaceOriginalPathRow = {
|
||||||
|
workspace_id: string;
|
||||||
workspace_path: string;
|
workspace_path: string;
|
||||||
custom_workspace_name: string | null;
|
custom_workspace_name: string | null;
|
||||||
isStarred: number; // SQLite boolean: 0 | 1
|
isStarred: number; // SQLite boolean: 0 | 1
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { RootLayout } from '@/components/refactored/shared/RootLayout';
|
|||||||
|
|
||||||
// Mock page components
|
// Mock page components
|
||||||
const Home = () => <div className="p-8"><h1>Home Page</h1><p>Select a session or create a new project.</p></div>;
|
const Home = () => <div className="p-8"><h1>Home Page</h1><p>Select a session or create a new project.</p></div>;
|
||||||
|
const WorkspaceContent = () => <div className="p-8"><h1>Workspace View</h1><p>Select a session or start a new one.</p></div>;
|
||||||
const SessionContent = () => <div className="p-8"><h1>Session View</h1><p>Chat interface goes here.</p></div>;
|
const SessionContent = () => <div className="p-8"><h1>Session View</h1><p>Chat interface goes here.</p></div>;
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
@@ -26,6 +27,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/sessions/:sessionId",
|
path: "/sessions/:sessionId",
|
||||||
element: <SessionContent />,
|
element: <SessionContent />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/workspace/:workspaceId",
|
||||||
|
element: <WorkspaceContent />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -57,16 +57,16 @@ export const getWorkspaceSessions = async (): Promise<WorkspaceRecord[]> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateWorkspaceStar = async (
|
export const updateWorkspaceStar = async (
|
||||||
workspacePath: string,
|
workspaceId: string,
|
||||||
): Promise<{ workspacePath: string; isStarred: boolean }> => {
|
): Promise<{ workspaceId: string; isStarred: boolean }> => {
|
||||||
const response = await authenticatedFetch(SIDEBAR_ENDPOINTS.updateWorkspaceStar, {
|
const response = await authenticatedFetch(SIDEBAR_ENDPOINTS.updateWorkspaceStar, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({ workspacePath }),
|
body: JSON.stringify({ workspaceId }),
|
||||||
});
|
});
|
||||||
const payload = await parseJsonSafely<{
|
const payload = await parseJsonSafely<{
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
data?: {
|
data?: {
|
||||||
workspacePath?: string;
|
workspaceId?: string;
|
||||||
isStarred?: boolean;
|
isStarred?: boolean;
|
||||||
};
|
};
|
||||||
error?: { message?: string };
|
error?: { message?: string };
|
||||||
@@ -80,18 +80,18 @@ export const updateWorkspaceStar = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspacePath: payload?.data?.workspacePath || workspacePath,
|
workspaceId: payload?.data?.workspaceId || workspaceId,
|
||||||
isStarred: Boolean(payload?.data?.isStarred),
|
isStarred: Boolean(payload?.data?.isStarred),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateWorkspaceCustomName = async (
|
export const updateWorkspaceCustomName = async (
|
||||||
workspacePath: string,
|
workspaceId: string,
|
||||||
workspaceCustomName: string | null,
|
workspaceCustomName: string | null,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const response = await authenticatedFetch(SIDEBAR_ENDPOINTS.updateWorkspaceCustomName, {
|
const response = await authenticatedFetch(SIDEBAR_ENDPOINTS.updateWorkspaceCustomName, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({ workspacePath, workspaceCustomName }),
|
body: JSON.stringify({ workspaceId, workspaceCustomName }),
|
||||||
});
|
});
|
||||||
const payload = await parseJsonSafely<{ error?: { message?: string } }>(response);
|
const payload = await parseJsonSafely<{ error?: { message?: string } }>(response);
|
||||||
|
|
||||||
@@ -103,10 +103,10 @@ export const updateWorkspaceCustomName = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteWorkspaceByPath = async (workspacePath: string): Promise<void> => {
|
export const deleteWorkspaceById = async (workspaceId: string): Promise<void> => {
|
||||||
const response = await authenticatedFetch(SIDEBAR_ENDPOINTS.deleteWorkspace, {
|
const response = await authenticatedFetch(SIDEBAR_ENDPOINTS.deleteWorkspace, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: JSON.stringify({ workspacePath }),
|
body: JSON.stringify({ workspaceId }),
|
||||||
});
|
});
|
||||||
const payload = await parseJsonSafely<{ error?: { message?: string } }>(response);
|
const payload = await parseJsonSafely<{ error?: { message?: string } }>(response);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deleteSessionById,
|
deleteSessionById,
|
||||||
deleteWorkspaceByPath,
|
deleteWorkspaceById,
|
||||||
getWorkspaceSessions,
|
getWorkspaceSessions,
|
||||||
updateSessionCustomName,
|
updateSessionCustomName,
|
||||||
updateWorkspaceCustomName,
|
updateWorkspaceCustomName,
|
||||||
@@ -95,7 +95,7 @@ export const useWorkspaces = () => {
|
|||||||
[filteredWorkspaces],
|
[filteredWorkspaces],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleWorkspace = useCallback((workspacePath: string) => {
|
const toggleWorkspace = useCallback((workspaceId: string, workspacePath: string) => {
|
||||||
setExpandedWorkspaces((previousSet) => {
|
setExpandedWorkspaces((previousSet) => {
|
||||||
const nextSet = new Set(previousSet);
|
const nextSet = new Set(previousSet);
|
||||||
|
|
||||||
@@ -107,7 +107,8 @@ export const useWorkspaces = () => {
|
|||||||
|
|
||||||
return nextSet;
|
return nextSet;
|
||||||
});
|
});
|
||||||
}, []);
|
navigate(`/workspace/${encodeURIComponent(workspaceId)}`);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
const openSession = useCallback(
|
const openSession = useCallback(
|
||||||
(workspacePath: string, sessionId: string) => {
|
(workspacePath: string, sessionId: string) => {
|
||||||
@@ -125,9 +126,9 @@ export const useWorkspaces = () => {
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const toggleWorkspaceStar = useCallback(async (workspacePath: string) => {
|
const toggleWorkspaceStar = useCallback(async (workspaceId: string) => {
|
||||||
try {
|
try {
|
||||||
await updateWorkspaceStar(workspacePath);
|
await updateWorkspaceStar(workspaceId);
|
||||||
await refreshWorkspaces();
|
await refreshWorkspaces();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update workspace star:', error);
|
console.error('Failed to update workspace star:', error);
|
||||||
@@ -149,10 +150,17 @@ export const useWorkspaces = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editingWorkspace = workspaces.find(
|
||||||
|
(workspace) => workspace.workspaceOriginalPath === editingWorkspacePath,
|
||||||
|
);
|
||||||
|
if (!editingWorkspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsSavingWorkspaceName(true);
|
setIsSavingWorkspaceName(true);
|
||||||
try {
|
try {
|
||||||
const trimmedName = editingWorkspaceName.trim();
|
const trimmedName = editingWorkspaceName.trim();
|
||||||
await updateWorkspaceCustomName(editingWorkspacePath, trimmedName || null);
|
await updateWorkspaceCustomName(editingWorkspace.workspaceId, trimmedName || null);
|
||||||
await refreshWorkspaces();
|
await refreshWorkspaces();
|
||||||
cancelWorkspaceRename();
|
cancelWorkspaceRename();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -165,10 +173,12 @@ export const useWorkspaces = () => {
|
|||||||
editingWorkspaceName,
|
editingWorkspaceName,
|
||||||
editingWorkspacePath,
|
editingWorkspacePath,
|
||||||
refreshWorkspaces,
|
refreshWorkspaces,
|
||||||
|
workspaces,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const requestWorkspaceDelete = useCallback((workspace: WorkspaceRecord) => {
|
const requestWorkspaceDelete = useCallback((workspace: WorkspaceRecord) => {
|
||||||
setWorkspaceDeleteTarget({
|
setWorkspaceDeleteTarget({
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
workspacePath: workspace.workspaceOriginalPath,
|
workspacePath: workspace.workspaceOriginalPath,
|
||||||
workspaceName: getWorkspaceDisplayName(workspace),
|
workspaceName: getWorkspaceDisplayName(workspace),
|
||||||
sessionCount: workspace.sessions.length,
|
sessionCount: workspace.sessions.length,
|
||||||
@@ -184,10 +194,11 @@ export const useWorkspaces = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deletingWorkspaceId = workspaceDeleteTarget.workspaceId;
|
||||||
const deletingWorkspacePath = workspaceDeleteTarget.workspacePath;
|
const deletingWorkspacePath = workspaceDeleteTarget.workspacePath;
|
||||||
setWorkspaceDeleteTarget(null);
|
setWorkspaceDeleteTarget(null);
|
||||||
try {
|
try {
|
||||||
await deleteWorkspaceByPath(deletingWorkspacePath);
|
await deleteWorkspaceById(deletingWorkspaceId);
|
||||||
|
|
||||||
// If the current session belonged to the deleted workspace, reset to root.
|
// If the current session belonged to the deleted workspace, reset to root.
|
||||||
const hadSelectedSession = workspaces.some(
|
const hadSelectedSession = workspaces.some(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type WorkspaceSession = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceRecord = {
|
export type WorkspaceRecord = {
|
||||||
|
workspaceId: string;
|
||||||
workspaceOriginalPath: string;
|
workspaceOriginalPath: string;
|
||||||
workspaceCustomName: string | null;
|
workspaceCustomName: string | null;
|
||||||
workspaceDisplayName: string;
|
workspaceDisplayName: string;
|
||||||
@@ -24,6 +25,7 @@ export type WorkspaceRecord = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceDeleteTarget = {
|
export type WorkspaceDeleteTarget = {
|
||||||
|
workspaceId: string;
|
||||||
workspacePath: string;
|
workspacePath: string;
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
sessionCount: number;
|
sessionCount: number;
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ type SidebarWorkspaceItemProps = {
|
|||||||
isSavingSessionName: boolean;
|
isSavingSessionName: boolean;
|
||||||
onEditingWorkspaceNameChange: (name: string) => void;
|
onEditingWorkspaceNameChange: (name: string) => void;
|
||||||
onEditingSessionNameChange: (name: string) => void;
|
onEditingSessionNameChange: (name: string) => void;
|
||||||
onToggleWorkspace: (workspacePath: string) => void;
|
onToggleWorkspace: (workspaceId: string, workspacePath: string) => void;
|
||||||
onToggleWorkspaceStar: (workspacePath: string) => void;
|
onToggleWorkspaceStar: (workspaceId: string) => void;
|
||||||
onStartWorkspaceRename: (workspace: WorkspaceRecord) => void;
|
onStartWorkspaceRename: (workspace: WorkspaceRecord) => void;
|
||||||
onCancelWorkspaceRename: () => void;
|
onCancelWorkspaceRename: () => void;
|
||||||
onSaveWorkspaceRename: () => void;
|
onSaveWorkspaceRename: () => void;
|
||||||
@@ -95,7 +95,7 @@ export function SidebarWorkspaceItem({
|
|||||||
!hasSelectedSession &&
|
!hasSelectedSession &&
|
||||||
'border-yellow-200/30 bg-yellow-50/50 dark:border-yellow-800/30 dark:bg-yellow-900/5',
|
'border-yellow-200/30 bg-yellow-50/50 dark:border-yellow-800/30 dark:bg-yellow-900/5',
|
||||||
)}
|
)}
|
||||||
onClick={() => onToggleWorkspace(workspace.workspaceOriginalPath)}
|
onClick={() => onToggleWorkspace(workspace.workspaceId, workspace.workspaceOriginalPath)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
@@ -179,7 +179,7 @@ export function SidebarWorkspaceItem({
|
|||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onToggleWorkspaceStar(workspace.workspaceOriginalPath);
|
onToggleWorkspaceStar(workspace.workspaceId);
|
||||||
}}
|
}}
|
||||||
title={workspace.isStarred ? 'Remove from Starred' : 'Add to Starred'}
|
title={workspace.isStarred ? 'Remove from Starred' : 'Add to Starred'}
|
||||||
>
|
>
|
||||||
@@ -236,7 +236,7 @@ export function SidebarWorkspaceItem({
|
|||||||
!hasSelectedSession &&
|
!hasSelectedSession &&
|
||||||
'bg-yellow-50/50 hover:bg-yellow-100/50 dark:bg-yellow-900/10 dark:hover:bg-yellow-900/20',
|
'bg-yellow-50/50 hover:bg-yellow-100/50 dark:bg-yellow-900/10 dark:hover:bg-yellow-900/20',
|
||||||
)}
|
)}
|
||||||
onClick={() => onToggleWorkspace(workspace.workspaceOriginalPath)}
|
onClick={() => onToggleWorkspace(workspace.workspaceId, workspace.workspaceOriginalPath)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
@@ -318,7 +318,7 @@ export function SidebarWorkspaceItem({
|
|||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onToggleWorkspaceStar(workspace.workspaceOriginalPath);
|
onToggleWorkspaceStar(workspace.workspaceId);
|
||||||
}}
|
}}
|
||||||
title={workspace.isStarred ? 'Remove from Starred' : 'Add to Starred'}
|
title={workspace.isStarred ? 'Remove from Starred' : 'Add to Starred'}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ type SidebarWorkspaceListProps = {
|
|||||||
isSavingSessionName: boolean;
|
isSavingSessionName: boolean;
|
||||||
onEditingWorkspaceNameChange: (name: string) => void;
|
onEditingWorkspaceNameChange: (name: string) => void;
|
||||||
onEditingSessionNameChange: (name: string) => void;
|
onEditingSessionNameChange: (name: string) => void;
|
||||||
onToggleWorkspace: (workspacePath: string) => void;
|
onToggleWorkspace: (workspaceId: string, workspacePath: string) => void;
|
||||||
onToggleWorkspaceStar: (workspacePath: string) => void;
|
onToggleWorkspaceStar: (workspaceId: string) => void;
|
||||||
onStartWorkspaceRename: (workspace: WorkspaceRecord) => void;
|
onStartWorkspaceRename: (workspace: WorkspaceRecord) => void;
|
||||||
onCancelWorkspaceRename: () => void;
|
onCancelWorkspaceRename: () => void;
|
||||||
onSaveWorkspaceRename: () => void;
|
onSaveWorkspaceRename: () => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user