mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-17 05:12:02 +08:00
Add a running-session view to the sidebar, including header controls, running counts, empty states, and row-level processing indicators so active provider work is visible outside the current chat. Hydrate running state after refresh through a status-only /api/providers/sessions/running endpoint backed by chatRunRegistry.listRunningRuns, then sync and poll the frontend processingSessions map from AppContent without attaching to chat streams or replaying messages. Preserve fresh local processing entries during sync so newly sent messages are not cleared before the backend registry catches up, and clear completed sessions once the status endpoint no longer reports them. Thread active session state through sidebar project/session components, show rotating loaders for processing sessions, and keep the running search mode expanded and filterable. Fix optimistic local user-message dedupe so repeated prompts are only collapsed when a matching server echo appears from the same send window, preventing sent messages from disappearing until assistant completion. Add registry test coverage for listing currently running app sessions. Tests: npx eslint on changed files; npx tsc --noEmit -p tsconfig.json; npx tsc --noEmit -p server/tsconfig.json; npx tsx --tsconfig server/tsconfig.json --test server/modules/websocket/tests/chat-run-registry.test.ts.
270 lines
9.6 KiB
JavaScript
270 lines
9.6 KiB
JavaScript
import { IS_PLATFORM } from "../constants/config";
|
|
|
|
// Utility function for authenticated API calls
|
|
export const authenticatedFetch = (url, options = {}) => {
|
|
const token = localStorage.getItem('auth-token');
|
|
|
|
const defaultHeaders = {};
|
|
|
|
// Only set Content-Type for non-FormData requests
|
|
if (!(options.body instanceof FormData)) {
|
|
defaultHeaders['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
if (!IS_PLATFORM && token) {
|
|
defaultHeaders['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
return fetch(url, {
|
|
...options,
|
|
headers: {
|
|
...defaultHeaders,
|
|
...options.headers,
|
|
},
|
|
}).then((response) => {
|
|
const refreshedToken = response.headers.get('X-Refreshed-Token');
|
|
if (refreshedToken) {
|
|
localStorage.setItem('auth-token', refreshedToken);
|
|
}
|
|
return response;
|
|
});
|
|
};
|
|
|
|
// API endpoints
|
|
export const api = {
|
|
// Auth endpoints (no token required)
|
|
auth: {
|
|
status: () => fetch('/api/auth/status'),
|
|
login: (username, password) => fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password }),
|
|
}),
|
|
register: (username, password) => fetch('/api/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password }),
|
|
}),
|
|
user: () => authenticatedFetch('/api/auth/user'),
|
|
logout: () => authenticatedFetch('/api/auth/logout', { method: 'POST' }),
|
|
},
|
|
|
|
// Protected endpoints
|
|
// config endpoint removed - no longer needed (frontend uses window.location)
|
|
// After the projectName → projectId migration the path/query identifier is
|
|
// the DB-assigned `projectId`; parameter names reflect that for clarity.
|
|
projects: () => authenticatedFetch('/api/projects'),
|
|
archivedProjects: () => authenticatedFetch('/api/projects/archived'),
|
|
projectSessions: (projectId, { limit = 20, offset = 0 } = {}) => {
|
|
const params = new URLSearchParams();
|
|
params.set('limit', String(limit));
|
|
params.set('offset', String(offset));
|
|
return authenticatedFetch(`/api/projects/${encodeURIComponent(projectId)}/sessions?${params.toString()}`);
|
|
},
|
|
projectTaskmaster: (projectId) =>
|
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectId)}/taskmaster`),
|
|
// Unified endpoint for persisted session messages.
|
|
// Provider/project metadata are resolved by the backend from sessionId.
|
|
unifiedSessionMessages: (sessionId, _provider = 'claude', { limit = null, offset = 0 } = {}) => {
|
|
const params = new URLSearchParams();
|
|
if (limit !== null) {
|
|
params.append('limit', String(limit));
|
|
params.append('offset', String(offset));
|
|
}
|
|
const queryString = params.toString();
|
|
return authenticatedFetch(`/api/providers/sessions/${encodeURIComponent(sessionId)}/messages${queryString ? `?${queryString}` : ''}`);
|
|
},
|
|
renameProject: (projectId, displayName) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/rename`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ displayName }),
|
|
}),
|
|
restoreProject: (projectId) =>
|
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectId)}/restore`, {
|
|
method: 'POST',
|
|
}),
|
|
// Session deletion now mirrors project deletion:
|
|
// - default: archive only (`isArchived = 1`)
|
|
// - hardDelete: remove the row and, by default, its persisted transcript file
|
|
deleteSession: (sessionId, hardDelete = false) => {
|
|
const params = new URLSearchParams();
|
|
if (hardDelete) {
|
|
params.set('force', 'true');
|
|
}
|
|
const qs = params.toString();
|
|
return authenticatedFetch(`/api/providers/sessions/${sessionId}${qs ? `?${qs}` : ''}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
getArchivedSessions: () =>
|
|
authenticatedFetch('/api/providers/sessions/archived'),
|
|
runningSessions: () =>
|
|
authenticatedFetch('/api/providers/sessions/running'),
|
|
restoreSession: (sessionId) =>
|
|
authenticatedFetch(`/api/providers/sessions/${sessionId}/restore`, {
|
|
method: 'POST',
|
|
}),
|
|
renameSession: (sessionId, summary) =>
|
|
authenticatedFetch(`/api/providers/sessions/${sessionId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ summary }),
|
|
}),
|
|
// `hardDelete` => server `?force=true` (remove DB row + Claude *.jsonl + sessions rows for path).
|
|
deleteProject: (projectId, hardDelete = false) => {
|
|
const params = new URLSearchParams();
|
|
if (hardDelete) params.set('force', 'true');
|
|
const qs = params.toString();
|
|
return authenticatedFetch(`/api/projects/${projectId}${qs ? `?${qs}` : ''}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
searchConversationsUrl: (query, limit = 50) => {
|
|
const token = localStorage.getItem('auth-token');
|
|
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
if (token) params.set('token', token);
|
|
return `/api/providers/search/sessions?${params.toString()}`;
|
|
},
|
|
createProject: (projectData) =>
|
|
authenticatedFetch('/api/projects/create-project', {
|
|
method: 'POST',
|
|
body: JSON.stringify(projectData),
|
|
}),
|
|
migrateLegacyProjectStars: (projectIds) =>
|
|
authenticatedFetch('/api/projects/migrate-legacy-stars', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ projectIds }),
|
|
}),
|
|
toggleProjectStar: (projectId) =>
|
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectId)}/toggle-star`, {
|
|
method: 'POST',
|
|
}),
|
|
readFile: (projectId, filePath) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/file?filePath=${encodeURIComponent(filePath)}`),
|
|
readFileBlob: (projectId, filePath) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/files/content?path=${encodeURIComponent(filePath)}`),
|
|
saveFile: (projectId, filePath, content) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/file`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ filePath, content }),
|
|
}),
|
|
getFiles: (projectId, options = {}) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/files`, options),
|
|
|
|
// File operations
|
|
createFile: (projectId, { path, type, name }) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/files/create`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ path, type, name }),
|
|
}),
|
|
|
|
renameFile: (projectId, { oldPath, newName }) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/files/rename`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ oldPath, newName }),
|
|
}),
|
|
|
|
deleteFile: (projectId, { path, type }) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/files`, {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ path, type }),
|
|
}),
|
|
|
|
uploadFiles: (projectId, formData) =>
|
|
authenticatedFetch(`/api/projects/${projectId}/files/upload`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {}, // Let browser set Content-Type for FormData
|
|
}),
|
|
|
|
// TaskMaster endpoints — all addressed by DB projectId post-migration.
|
|
taskmaster: {
|
|
// Initialize TaskMaster in a project
|
|
init: (projectId) =>
|
|
authenticatedFetch(`/api/taskmaster/init/${projectId}`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
// Add a new task
|
|
addTask: (projectId, { prompt, title, description, priority, dependencies }) =>
|
|
authenticatedFetch(`/api/taskmaster/add-task/${projectId}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ prompt, title, description, priority, dependencies }),
|
|
}),
|
|
|
|
// Parse PRD to generate tasks
|
|
parsePRD: (projectId, { fileName, numTasks, append }) =>
|
|
authenticatedFetch(`/api/taskmaster/parse-prd/${projectId}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ fileName, numTasks, append }),
|
|
}),
|
|
|
|
// Get available PRD templates
|
|
getTemplates: () =>
|
|
authenticatedFetch('/api/taskmaster/prd-templates'),
|
|
|
|
// Apply a PRD template
|
|
applyTemplate: (projectId, { templateId, fileName, customizations }) =>
|
|
authenticatedFetch(`/api/taskmaster/apply-template/${projectId}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ templateId, fileName, customizations }),
|
|
}),
|
|
|
|
// Update a task
|
|
updateTask: (projectId, taskId, updates) =>
|
|
authenticatedFetch(`/api/taskmaster/update-task/${projectId}/${taskId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(updates),
|
|
}),
|
|
},
|
|
|
|
// Browse filesystem for project suggestions
|
|
browseFilesystem: (dirPath = null) => {
|
|
const params = new URLSearchParams();
|
|
if (dirPath) params.append('path', dirPath);
|
|
|
|
return authenticatedFetch(`/api/browse-filesystem?${params}`);
|
|
},
|
|
|
|
createFolder: (folderPath) =>
|
|
authenticatedFetch('/api/create-folder', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ path: folderPath }),
|
|
}),
|
|
|
|
// User endpoints
|
|
user: {
|
|
gitConfig: () => authenticatedFetch('/api/user/git-config'),
|
|
updateGitConfig: (gitName, gitEmail) =>
|
|
authenticatedFetch('/api/user/git-config', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ gitName, gitEmail }),
|
|
}),
|
|
onboardingStatus: () => authenticatedFetch('/api/user/onboarding-status'),
|
|
completeOnboarding: () =>
|
|
authenticatedFetch('/api/user/complete-onboarding', {
|
|
method: 'POST',
|
|
}),
|
|
},
|
|
|
|
// Generic GET method for any endpoint
|
|
get: (endpoint) => authenticatedFetch(`/api${endpoint}`),
|
|
|
|
// Generic POST method for any endpoint
|
|
post: (endpoint, body) => authenticatedFetch(`/api${endpoint}`, {
|
|
method: 'POST',
|
|
...(body instanceof FormData ? { body } : { body: JSON.stringify(body) }),
|
|
}),
|
|
|
|
// Generic PUT method for any endpoint
|
|
put: (endpoint, body) => authenticatedFetch(`/api${endpoint}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
// Generic DELETE method for any endpoint
|
|
delete: (endpoint, options = {}) => authenticatedFetch(`/api${endpoint}`, {
|
|
method: 'DELETE',
|
|
...options,
|
|
}),
|
|
};
|