Files
claudecodeui/src/utils/api.js
Haileyesus 591b18e9e3 feat(sidebar): improve running session state tracking
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.
2026-06-11 20:04:38 +03:00

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,
}),
};