mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-01 01:45:33 +08:00
feat: show session name and workspace path in heading
This commit is contained in:
@@ -3,6 +3,7 @@ import fsp, { readFile } from 'node:fs/promises';
|
|||||||
|
|
||||||
import { scanStateDb } from '@/shared/database/repositories/scan-state.db.js';
|
import { scanStateDb } from '@/shared/database/repositories/scan-state.db.js';
|
||||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||||
|
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
|
||||||
import type { LLMProvider } from '@/shared/types/app.js';
|
import type { LLMProvider } from '@/shared/types/app.js';
|
||||||
import { AppError } from '@/shared/utils/app-error.js';
|
import { AppError } from '@/shared/utils/app-error.js';
|
||||||
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
||||||
@@ -118,7 +119,11 @@ export const llmSessionsService = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return session;
|
const workspace = workspaceOriginalPathsDb.getWorkspacePath(session.workspace_path);
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
workspace_id: workspace?.workspace_id ?? null,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import test from 'node:test';
|
|||||||
import { AppError } from '@/shared/utils/app-error.js';
|
import { AppError } from '@/shared/utils/app-error.js';
|
||||||
import { scanStateDb } from '@/shared/database/repositories/scan-state.db.js';
|
import { scanStateDb } from '@/shared/database/repositories/scan-state.db.js';
|
||||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||||
|
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
|
||||||
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
||||||
import { llmSessionsService } from '@/modules/ai-runtime/services/sessions.service.js';
|
import { llmSessionsService } from '@/modules/ai-runtime/services/sessions.service.js';
|
||||||
import { conversationSearchService } from '@/modules/conversations/conversation-search.service.js';
|
import { conversationSearchService } from '@/modules/conversations/conversation-search.service.js';
|
||||||
@@ -102,6 +103,7 @@ test('llmSessionsService.updateSessionCustomName validates existence before upda
|
|||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
workspace_path: '/tmp/workspace',
|
workspace_path: '/tmp/workspace',
|
||||||
jsonl_path: null,
|
jsonl_path: null,
|
||||||
|
custom_name: null,
|
||||||
created_at: '2026-04-01T00:00:00.000Z',
|
created_at: '2026-04-01T00:00:00.000Z',
|
||||||
updated_at: '2026-04-01T00:00:00.000Z',
|
updated_at: '2026-04-01T00:00:00.000Z',
|
||||||
}
|
}
|
||||||
@@ -140,11 +142,22 @@ test('llmSessionsService.getIndexedSession returns DB session metadata', { concu
|
|||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
workspace_path: '/tmp/workspace',
|
workspace_path: '/tmp/workspace',
|
||||||
jsonl_path: '/tmp/workspace/session.jsonl',
|
jsonl_path: '/tmp/workspace/session.jsonl',
|
||||||
|
custom_name: 'Custom Session Name',
|
||||||
created_at: '2026-04-01T00:00:00.000Z',
|
created_at: '2026-04-01T00:00:00.000Z',
|
||||||
updated_at: '2026-04-02T00:00:00.000Z',
|
updated_at: '2026-04-02T00:00:00.000Z',
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
));
|
));
|
||||||
|
const restoreGetWorkspacePath = patchMethod(workspaceOriginalPathsDb, 'getWorkspacePath', (workspacePath: string) => (
|
||||||
|
workspacePath === '/tmp/workspace'
|
||||||
|
? {
|
||||||
|
workspace_id: 'workspace-123',
|
||||||
|
workspace_path: workspacePath,
|
||||||
|
custom_workspace_name: 'Workspace Custom Name',
|
||||||
|
isStarred: 0,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = llmSessionsService.getIndexedSession('known-session');
|
const session = llmSessionsService.getIndexedSession('known-session');
|
||||||
@@ -153,8 +166,10 @@ test('llmSessionsService.getIndexedSession returns DB session metadata', { concu
|
|||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
workspace_path: '/tmp/workspace',
|
workspace_path: '/tmp/workspace',
|
||||||
jsonl_path: '/tmp/workspace/session.jsonl',
|
jsonl_path: '/tmp/workspace/session.jsonl',
|
||||||
|
custom_name: 'Custom Session Name',
|
||||||
created_at: '2026-04-01T00:00:00.000Z',
|
created_at: '2026-04-01T00:00:00.000Z',
|
||||||
updated_at: '2026-04-02T00:00:00.000Z',
|
updated_at: '2026-04-02T00:00:00.000Z',
|
||||||
|
workspace_id: 'workspace-123',
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
@@ -165,6 +180,7 @@ test('llmSessionsService.getIndexedSession returns DB session metadata', { concu
|
|||||||
error.statusCode === 404,
|
error.statusCode === 404,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
restoreGetWorkspacePath();
|
||||||
restoreGetById();
|
restoreGetById();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -183,6 +199,7 @@ test('llmSessionsService.deleteSessionArtifacts validates ids and deletes disk/d
|
|||||||
provider: 'cursor',
|
provider: 'cursor',
|
||||||
workspace_path: '/tmp/workspace',
|
workspace_path: '/tmp/workspace',
|
||||||
jsonl_path: transcriptPath,
|
jsonl_path: transcriptPath,
|
||||||
|
custom_name: null,
|
||||||
created_at: '2026-04-01T00:00:00.000Z',
|
created_at: '2026-04-01T00:00:00.000Z',
|
||||||
updated_at: '2026-04-01T00:00:00.000Z',
|
updated_at: '2026-04-01T00:00:00.000Z',
|
||||||
}
|
}
|
||||||
@@ -233,6 +250,7 @@ test('llmSessionsService.getSessionHistory parses JSONL and Gemini JSON correctl
|
|||||||
provider: 'cursor',
|
provider: 'cursor',
|
||||||
workspace_path: '/tmp/workspace',
|
workspace_path: '/tmp/workspace',
|
||||||
jsonl_path: jsonlPath,
|
jsonl_path: jsonlPath,
|
||||||
|
custom_name: null,
|
||||||
created_at: '2026-04-01T00:00:00.000Z',
|
created_at: '2026-04-01T00:00:00.000Z',
|
||||||
updated_at: '2026-04-01T00:00:00.000Z',
|
updated_at: '2026-04-01T00:00:00.000Z',
|
||||||
};
|
};
|
||||||
@@ -244,6 +262,7 @@ test('llmSessionsService.getSessionHistory parses JSONL and Gemini JSON correctl
|
|||||||
provider: 'gemini',
|
provider: 'gemini',
|
||||||
workspace_path: '/tmp/workspace',
|
workspace_path: '/tmp/workspace',
|
||||||
jsonl_path: jsonPath,
|
jsonl_path: jsonPath,
|
||||||
|
custom_name: null,
|
||||||
created_at: '2026-04-01T00:00:00.000Z',
|
created_at: '2026-04-01T00:00:00.000Z',
|
||||||
updated_at: '2026-04-01T00:00:00.000Z',
|
updated_at: '2026-04-01T00:00:00.000Z',
|
||||||
};
|
};
|
||||||
@@ -255,6 +274,7 @@ test('llmSessionsService.getSessionHistory parses JSONL and Gemini JSON correctl
|
|||||||
provider: 'claude',
|
provider: 'claude',
|
||||||
workspace_path: '/tmp/workspace',
|
workspace_path: '/tmp/workspace',
|
||||||
jsonl_path: null,
|
jsonl_path: null,
|
||||||
|
custom_name: null,
|
||||||
created_at: '2026-04-01T00:00:00.000Z',
|
created_at: '2026-04-01T00:00:00.000Z',
|
||||||
updated_at: '2026-04-01T00:00:00.000Z',
|
updated_at: '2026-04-01T00:00:00.000Z',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ const parseWorkspaceIdFromBody = (req: Request): string => {
|
|||||||
return workspaceId;
|
return workspaceId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseWorkspaceIdFromParams = (req: Request): string => {
|
||||||
|
const workspaceId = getTrimmedString(req.params.workspaceId);
|
||||||
|
if (!workspaceId) {
|
||||||
|
throw new AppError('workspaceId is required.', {
|
||||||
|
code: 'WORKSPACE_ID_REQUIRED',
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspaceId;
|
||||||
|
};
|
||||||
|
|
||||||
const parseWorkspaceCustomNameFromBody = (req: Request): string | null => {
|
const parseWorkspaceCustomNameFromBody = (req: Request): string | null => {
|
||||||
const body = req.body as Record<string, unknown> | undefined;
|
const body = req.body as Record<string, unknown> | undefined;
|
||||||
const customName = getTrimmedString(body?.workspaceCustomName);
|
const customName = getTrimmedString(body?.workspaceCustomName);
|
||||||
@@ -43,6 +55,15 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId',
|
||||||
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const workspaceId = parseWorkspaceIdFromParams(req);
|
||||||
|
const workspace = workspaceService.getWorkspaceById(workspaceId);
|
||||||
|
res.json(createApiSuccessResponse({ workspace }));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
'/star',
|
'/star',
|
||||||
asyncHandler(async (req: Request, res: Response) => {
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -86,6 +86,30 @@ const sortWorkspacesByLastActivity = (
|
|||||||
return left.workspaceDisplayName.localeCompare(right.workspaceDisplayName);
|
return left.workspaceDisplayName.localeCompare(right.workspaceDisplayName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toWorkspaceRecord = (
|
||||||
|
workspaceId: string,
|
||||||
|
workspacePath: string,
|
||||||
|
workspaceCustomName: string | null,
|
||||||
|
isStarred: boolean,
|
||||||
|
sessions: WorkspaceSessionRecord[],
|
||||||
|
): WorkspaceRecord => {
|
||||||
|
const sortedSessions = sortSessionsByLastActivity(sessions);
|
||||||
|
const lastActivity = sortedSessions[0]?.lastActivity || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspaceId,
|
||||||
|
workspaceOriginalPath: workspacePath,
|
||||||
|
workspaceCustomName,
|
||||||
|
workspaceDisplayName:
|
||||||
|
workspaceCustomName ||
|
||||||
|
path.basename(workspacePath) ||
|
||||||
|
workspacePath,
|
||||||
|
isStarred,
|
||||||
|
lastActivity,
|
||||||
|
sessions: sortedSessions,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups indexed sessions by workspace and returns a deterministic catalog shape.
|
* Groups indexed sessions by workspace and returns a deterministic catalog shape.
|
||||||
*/
|
*/
|
||||||
@@ -102,23 +126,14 @@ const buildWorkspaceSessionCollection = (): WorkspaceRecord[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceRecords = workspaceRows.map((workspaceRow) => {
|
const workspaceRecords = workspaceRows.map((workspaceRow) => {
|
||||||
const sessions = sortSessionsByLastActivity(
|
const sessions = sessionsByWorkspace.get(workspaceRow.workspace_path) || [];
|
||||||
sessionsByWorkspace.get(workspaceRow.workspace_path) || [],
|
return toWorkspaceRecord(
|
||||||
);
|
workspaceRow.workspace_id,
|
||||||
const lastActivity = sessions[0]?.lastActivity || null;
|
workspaceRow.workspace_path,
|
||||||
|
workspaceRow.custom_workspace_name,
|
||||||
return {
|
workspaceRow.isStarred === 1,
|
||||||
workspaceId: workspaceRow.workspace_id,
|
|
||||||
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,
|
sessions,
|
||||||
};
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return sortWorkspacesByLastActivity(workspaceRecords);
|
return sortWorkspacesByLastActivity(workspaceRecords);
|
||||||
@@ -132,6 +147,28 @@ export const workspaceService = {
|
|||||||
return buildWorkspaceSessionCollection();
|
return buildWorkspaceSessionCollection();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getWorkspaceById(workspaceId: string): WorkspaceRecord {
|
||||||
|
const workspaceRow = workspaceOriginalPathsDb.getWorkspaceById(workspaceId);
|
||||||
|
if (!workspaceRow) {
|
||||||
|
throw new AppError('Workspace not found.', {
|
||||||
|
code: 'WORKSPACE_NOT_FOUND',
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions = sessionsDb
|
||||||
|
.getSessionsByWorkspacePath(workspaceRow.workspace_path)
|
||||||
|
.map(toWorkspaceSessionRecord);
|
||||||
|
|
||||||
|
return toWorkspaceRecord(
|
||||||
|
workspaceRow.workspace_id,
|
||||||
|
workspaceRow.workspace_path,
|
||||||
|
workspaceRow.custom_workspace_name,
|
||||||
|
workspaceRow.isStarred === 1,
|
||||||
|
sessions,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
toggleWorkspaceStar(workspaceId: string): boolean {
|
toggleWorkspaceStar(workspaceId: string): boolean {
|
||||||
const workspaceRow = workspaceOriginalPathsDb.getWorkspaceById(workspaceId);
|
const workspaceRow = workspaceOriginalPathsDb.getWorkspaceById(workspaceId);
|
||||||
if (!workspaceRow) {
|
if (!workspaceRow) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type SessionNameLookupRow = {
|
|||||||
|
|
||||||
type SessionMetadataLookupRow = Pick<
|
type SessionMetadataLookupRow = Pick<
|
||||||
SessionsRow,
|
SessionsRow,
|
||||||
'session_id' | 'provider' | 'workspace_path' | 'jsonl_path' | 'created_at' | 'updated_at'
|
'session_id' | 'provider' | 'workspace_path' | 'jsonl_path' | 'custom_name' | 'created_at' | 'updated_at'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function normalizeTimestamp(value?: string): string | null {
|
function normalizeTimestamp(value?: string): string | null {
|
||||||
@@ -119,7 +119,7 @@ export const sessionsDb = {
|
|||||||
const db = getConnection();
|
const db = getConnection();
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT session_id, provider, workspace_path, jsonl_path, created_at, updated_at
|
`SELECT session_id, provider, workspace_path, jsonl_path, custom_name, created_at, updated_at
|
||||||
FROM sessions
|
FROM sessions
|
||||||
WHERE session_id = ?`
|
WHERE session_id = ?`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { AppTab } from '@/types/app';
|
import type { AppTab, SessionProvider } from '@/types/app';
|
||||||
import { usePlugins } from '@/contexts/PluginsContext';
|
import { usePlugins } from '@/contexts/PluginsContext';
|
||||||
import { useDeviceSettings } from '@/hooks/useDeviceSettings';
|
import { useDeviceSettings } from '@/hooks/useDeviceSettings';
|
||||||
import { useSystemUI } from '@/components/refactored/shared/contexts/system-ui-context/useSystemUI';
|
import { useSystemUI } from '@/components/refactored/shared/contexts/system-ui-context/useSystemUI';
|
||||||
import { MainHeadingTabSwitcher } from '@/components/refactored/shared/layout/heading/MainHeadingTabSwitcher';
|
import { MainHeadingTabSwitcher } from '@/components/refactored/shared/layout/heading/MainHeadingTabSwitcher';
|
||||||
|
import { getSessionById, getWorkspaceById } from '@/components/refactored/sidebar/data/workspacesApi';
|
||||||
|
|
||||||
type MainHeadingRouteParams = {
|
type MainHeadingRouteParams = {
|
||||||
workspaceId?: string;
|
workspaceId?: string;
|
||||||
@@ -60,6 +61,20 @@ const getTabTitle = (tab: AppTab, pluginDisplayName: string | undefined, t: (key
|
|||||||
return t('tabs.chat');
|
return t('tabs.chat');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStoredModelForProvider = (provider: SessionProvider): string => {
|
||||||
|
const storageKeyByProvider: Record<SessionProvider, string> = {
|
||||||
|
claude: 'claude-model',
|
||||||
|
cursor: 'cursor-model',
|
||||||
|
codex: 'codex-model',
|
||||||
|
gemini: 'gemini-model',
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = localStorage.getItem(storageKeyByProvider[provider]);
|
||||||
|
return typeof model === 'string' ? model.trim() : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTrimmedOrEmpty = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
|
||||||
|
|
||||||
export function MainHeading() {
|
export function MainHeading() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation(['common', 'sidebar']);
|
const { t } = useTranslation(['common', 'sidebar']);
|
||||||
@@ -72,6 +87,9 @@ export function MainHeading() {
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||||
|
const [resolvedWorkspacePath, setResolvedWorkspacePath] = useState('');
|
||||||
|
const [resolvedSessionTitle, setResolvedSessionTitle] = useState('');
|
||||||
|
const [resolvedSessionWorkspacePath, setResolvedSessionWorkspacePath] = useState('');
|
||||||
|
|
||||||
const decodedWorkspaceId = useMemo(() => decodeValue(workspaceId), [workspaceId]);
|
const decodedWorkspaceId = useMemo(() => decodeValue(workspaceId), [workspaceId]);
|
||||||
const decodedSessionId = useMemo(() => decodeValue(sessionId), [sessionId]);
|
const decodedSessionId = useMemo(() => decodeValue(sessionId), [sessionId]);
|
||||||
@@ -96,9 +114,100 @@ export function MainHeading() {
|
|||||||
[activeTab, plugins],
|
[activeTab, plugins],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!decodedWorkspaceId) {
|
||||||
|
setResolvedWorkspacePath('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
const loadWorkspace = async () => {
|
||||||
|
try {
|
||||||
|
const workspace = await getWorkspaceById(decodedWorkspaceId);
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathValue =
|
||||||
|
getTrimmedOrEmpty(workspace.workspaceOriginalPath) ||
|
||||||
|
decodedWorkspaceId;
|
||||||
|
setResolvedWorkspacePath(pathValue);
|
||||||
|
} catch {
|
||||||
|
if (!disposed) {
|
||||||
|
setResolvedWorkspacePath(decodedWorkspaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadWorkspace();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
}, [decodedWorkspaceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!decodedSessionId) {
|
||||||
|
setResolvedSessionTitle('');
|
||||||
|
setResolvedSessionWorkspacePath('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
const loadSession = async () => {
|
||||||
|
try {
|
||||||
|
const session = await getSessionById(decodedSessionId);
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customName = getTrimmedOrEmpty(session.custom_name);
|
||||||
|
const modelName = getStoredModelForProvider(session.provider);
|
||||||
|
setResolvedSessionTitle(customName || modelName || decodedSessionId);
|
||||||
|
setResolvedSessionWorkspacePath(getTrimmedOrEmpty(session.workspace_path));
|
||||||
|
|
||||||
|
const workspaceIdFromSession = getTrimmedOrEmpty(session.workspace_id);
|
||||||
|
if (!workspaceIdFromSession) {
|
||||||
|
if (!decodedWorkspaceId) {
|
||||||
|
setResolvedWorkspacePath(getTrimmedOrEmpty(session.workspace_path));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workspace = await getWorkspaceById(workspaceIdFromSession);
|
||||||
|
if (disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspacePath =
|
||||||
|
getTrimmedOrEmpty(workspace.workspaceOriginalPath) ||
|
||||||
|
workspaceIdFromSession;
|
||||||
|
setResolvedWorkspacePath(workspacePath);
|
||||||
|
} catch {
|
||||||
|
if (!disposed && !decodedWorkspaceId) {
|
||||||
|
setResolvedWorkspacePath(getTrimmedOrEmpty(session.workspace_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!disposed) {
|
||||||
|
setResolvedSessionTitle(decodedSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadSession();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
}, [decodedSessionId, decodedWorkspaceId]);
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (activeTab === 'chat' && decodedSessionId) {
|
if (activeTab === 'chat' && decodedSessionId) {
|
||||||
return decodedSessionId;
|
return resolvedSessionTitle || decodedSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTab === 'chat') {
|
if (activeTab === 'chat') {
|
||||||
@@ -106,7 +215,14 @@ export function MainHeading() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return getTabTitle(activeTab, pluginDisplayName, t);
|
return getTabTitle(activeTab, pluginDisplayName, t);
|
||||||
}, [activeTab, decodedSessionId, pluginDisplayName, t]);
|
}, [activeTab, decodedSessionId, pluginDisplayName, resolvedSessionTitle, t]);
|
||||||
|
|
||||||
|
const subtitle = useMemo(() => (
|
||||||
|
resolvedSessionWorkspacePath ||
|
||||||
|
resolvedWorkspacePath ||
|
||||||
|
decodedWorkspaceId ||
|
||||||
|
t('mainContent.newSession')
|
||||||
|
), [decodedWorkspaceId, resolvedSessionWorkspacePath, resolvedWorkspacePath, t]);
|
||||||
|
|
||||||
const updateScrollState = useCallback(() => {
|
const updateScrollState = useCallback(() => {
|
||||||
const element = scrollRef.current;
|
const element = scrollRef.current;
|
||||||
@@ -180,7 +296,7 @@ export function MainHeading() {
|
|||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="truncate text-[11px] leading-tight text-muted-foreground">
|
<div className="truncate text-[11px] leading-tight text-muted-foreground">
|
||||||
{decodedWorkspaceId || t('mainContent.newSession')}
|
{subtitle}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { WorkspaceRecord } from '@/components/refactored/sidebar/types';
|
import type { SessionMetadataRecord, WorkspaceRecord } from '@/components/refactored/sidebar/types';
|
||||||
import { authenticatedFetch } from '@/utils/api';
|
import { authenticatedFetch } from '@/utils/api';
|
||||||
|
|
||||||
const SIDEBAR_ENDPOINTS = {
|
const SIDEBAR_ENDPOINTS = {
|
||||||
@@ -56,6 +56,54 @@ export const getWorkspaceSessions = async (): Promise<WorkspaceRecord[]> => {
|
|||||||
return payload?.data?.workspaces || [];
|
return payload?.data?.workspaces || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWorkspaceById = async (workspaceId: string): Promise<WorkspaceRecord> => {
|
||||||
|
const response = await authenticatedFetch(
|
||||||
|
`${SIDEBAR_ENDPOINTS.getWorkspaceSessions}/${encodeURIComponent(workspaceId)}`,
|
||||||
|
);
|
||||||
|
const payload = await parseJsonSafely<{
|
||||||
|
success?: boolean;
|
||||||
|
data?: { workspace?: WorkspaceRecord };
|
||||||
|
error?: { message?: string };
|
||||||
|
}>(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
payload?.error?.message ||
|
||||||
|
getErrorMessage('Failed to fetch workspace', payload),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = payload?.data?.workspace;
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Workspace not found in response payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSessionById = async (sessionId: string): Promise<SessionMetadataRecord> => {
|
||||||
|
const response = await authenticatedFetch(`/api/llm/sessions/${encodeURIComponent(sessionId)}`);
|
||||||
|
const payload = await parseJsonSafely<{
|
||||||
|
success?: boolean;
|
||||||
|
data?: { session?: SessionMetadataRecord };
|
||||||
|
error?: { message?: string };
|
||||||
|
}>(response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
payload?.error?.message ||
|
||||||
|
getErrorMessage('Failed to fetch session', payload),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = payload?.data?.session;
|
||||||
|
if (!session) {
|
||||||
|
throw new Error('Session not found in response payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
export const updateWorkspaceStar = async (
|
export const updateWorkspaceStar = async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<{ workspaceId: string; isStarred: boolean }> => {
|
): Promise<{ workspaceId: string; isStarred: boolean }> => {
|
||||||
|
|||||||
@@ -41,3 +41,16 @@ export type WorkspaceGroups = {
|
|||||||
starred: WorkspaceRecord[];
|
starred: WorkspaceRecord[];
|
||||||
unstarred: WorkspaceRecord[];
|
unstarred: WorkspaceRecord[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// -------- SESSION TYPES --------
|
||||||
|
export type SessionMetadataRecord = {
|
||||||
|
session_id: string;
|
||||||
|
provider: SessionProvider;
|
||||||
|
workspace_path: string;
|
||||||
|
workspace_id: string | null;
|
||||||
|
custom_name: string | null;
|
||||||
|
jsonl_path: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user