feat: Add batch delete functionality for chat sessions (#480)
* feat: add batch delete functionality for chat sessions Backend: - Add batchRemove controller to handle bulk session deletion - Add POST /api/hermes/sessions/batch-delete endpoint - Support both local session store and CLI deletion - Return detailed results (deleted, failed, errors) Frontend: - Add batch selection mode with checkboxes in SessionListItem - Add batch selection toggle and select all button - Add batch delete button with confirmation - Update ChatPanel to manage selected session IDs - Add batchDeleteSessions API function i18n: - Add batch delete translations for all 8 languages - Simplify "Web UI/API Server Sessions" to "Sessions" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: vertically align buttons in session list header Add inline-flex and center alignment to all buttons in session-list-actions to ensure proper vertical centering with the title text. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: ensure proper vertical alignment in session list header - Set fixed height of 22px for session-list-actions - Add min-height and height to all buttons - Add line-height to session-list-title for text baseline alignment - Add min-height: 0 to session-list-header to prevent flex stretch This ensures the title and all action buttons are perfectly vertically centered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: call loadSessions after batch delete instead of looping deleteSession The previous implementation was calling chatStore.deleteSession(id) in a loop after batch delete API succeeded, which triggered individual delete API calls for each session - causing n API requests instead of 1. Now we simply call loadSessions() to refresh the session list from the server after successful batch deletion, ensuring: - Only 1 API request for batch delete - UI stays in sync with server state - No duplicate API calls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: improve update mechanism reliability Major improvements to the update system: **Path Resolution:** - Remove unreliable dirname(process.execPath) assumption - Use npm from PATH environment variable - Dynamically get global prefix via `npm prefix -g` - Calculate CLI path based on actual global install location **Windows Support:** - Remove complex cmd.exe wrapper logic - Directly call npm.cmd (works on all Windows setups) - Simplified quote handling **Error Handling:** - Add fallback error message (err.stderr || err.message || String(err)) - Add default success message when output is empty - Wrap spawnRestart in try-finally to ensure cleanup **Timing:** - Increase timeout from 120s to 10min (slow network support) - Increase restart delay from 2s to 3s (safer margin) **Code Quality:** - Remove unused functions (getNodeBinDir, getWindowsShell, quoteForWindowsCommand) - Use constants instead of magic numbers (10 * 60 * 1000) - More maintainable and cross-platform compatible This fixes issues where updates would fail due to incorrect npm/CLI paths on systems with non-standard Node.js installations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -284,6 +284,54 @@ export async function remove(ctx: any) {
|
||||
ctx.body = { ok: true }
|
||||
}
|
||||
|
||||
export async function batchRemove(ctx: any) {
|
||||
const { ids } = ctx.request.body as { ids?: string[] }
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'ids is required and must be a non-empty array' }
|
||||
return
|
||||
}
|
||||
|
||||
const validIds = ids.filter(id => typeof id === 'string' && id.trim() !== '')
|
||||
if (validIds.length === 0) {
|
||||
ctx.status = 400
|
||||
ctx.body = { error: 'No valid session ids provided' }
|
||||
return
|
||||
}
|
||||
|
||||
const results = {
|
||||
deleted: 0,
|
||||
failed: 0,
|
||||
errors: [] as Array<{ id: string; error: string }>
|
||||
}
|
||||
|
||||
if (useLocalSessionStore()) {
|
||||
for (const id of validIds) {
|
||||
const ok = localDeleteSession(id)
|
||||
if (ok) {
|
||||
deleteUsage(id)
|
||||
results.deleted++
|
||||
} else {
|
||||
results.failed++
|
||||
results.errors.push({ id, error: 'Failed to delete session' })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const id of validIds) {
|
||||
const ok = await hermesCli.deleteSession(id)
|
||||
if (ok) {
|
||||
deleteUsage(id)
|
||||
results.deleted++
|
||||
} else {
|
||||
results.failed++
|
||||
results.errors.push({ id, error: 'Failed to delete session' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.body = { ...results, ok: true }
|
||||
}
|
||||
|
||||
export async function usageBatch(ctx: any) {
|
||||
const ids = (ctx.query.ids as string)
|
||||
if (!ids) {
|
||||
|
||||
@@ -1,53 +1,39 @@
|
||||
import { execFileSync, spawn } from 'child_process'
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
function getNodeBinDir() {
|
||||
return dirname(process.execPath)
|
||||
}
|
||||
import { join } from 'path'
|
||||
|
||||
function getNpmBin() {
|
||||
return join(getNodeBinDir(), process.platform === 'win32' ? 'npm.cmd' : 'npm')
|
||||
return process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
||||
}
|
||||
|
||||
function getCliBin() {
|
||||
return join(getNodeBinDir(), process.platform === 'win32' ? 'hermes-web-ui.cmd' : 'hermes-web-ui')
|
||||
function getGlobalPrefix() {
|
||||
return execFileSync(getNpmBin(), ['prefix', '-g'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
}).trim()
|
||||
}
|
||||
|
||||
function getWindowsShell() {
|
||||
return process.env.ComSpec || 'cmd.exe'
|
||||
}
|
||||
function getGlobalCliBin() {
|
||||
const prefix = getGlobalPrefix()
|
||||
|
||||
function quoteForWindowsCommand(value: string) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
if (process.platform === 'win32') {
|
||||
return join(prefix, 'hermes-web-ui.cmd')
|
||||
}
|
||||
|
||||
return join(prefix, 'bin', 'hermes-web-ui')
|
||||
}
|
||||
|
||||
function runUpdateInstall() {
|
||||
if (process.platform === 'win32') {
|
||||
return execFileSync(getWindowsShell(), ['/d', '/s', '/c', `${quoteForWindowsCommand(getNpmBin())} install -g hermes-web-ui@latest`], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 120000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
})
|
||||
}
|
||||
|
||||
return execFileSync(getNpmBin(), ['install', '-g', 'hermes-web-ui@latest'], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 120000,
|
||||
timeout: 10 * 60 * 1000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
})
|
||||
}
|
||||
|
||||
function spawnRestart(port: string) {
|
||||
if (process.platform === 'win32') {
|
||||
return spawn(getWindowsShell(), ['/d', '/s', '/c', `${quoteForWindowsCommand(getCliBin())} restart --port ${port}`], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
})
|
||||
}
|
||||
const cli = getGlobalCliBin()
|
||||
|
||||
return spawn(getCliBin(), ['restart', '--port', port], {
|
||||
return spawn(cli, ['restart', '--port', port], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
@@ -57,13 +43,24 @@ function spawnRestart(port: string) {
|
||||
export async function handleUpdate(ctx: any) {
|
||||
try {
|
||||
const output = runUpdateInstall()
|
||||
ctx.body = { success: true, message: output.trim() }
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
message: output.trim() || 'hermes-web-ui updated successfully',
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
spawnRestart(process.env.PORT || '8648').unref()
|
||||
process.exit(0)
|
||||
}, 2000)
|
||||
try {
|
||||
spawnRestart(process.env.PORT || '8648').unref()
|
||||
} finally {
|
||||
process.exit(0)
|
||||
}
|
||||
}, 3000)
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
ctx.body = { success: false, message: err.stderr || err.message }
|
||||
ctx.body = {
|
||||
success: false,
|
||||
message: err.stderr?.toString() || err.message || String(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ sessionRoutes.get('/api/hermes/sessions/context-length', ctrl.contextLength)
|
||||
sessionRoutes.get('/api/hermes/sessions/:id', ctrl.get)
|
||||
sessionRoutes.get('/api/hermes/sessions/:id/usage', ctrl.usageSingle)
|
||||
sessionRoutes.delete('/api/hermes/sessions/:id', ctrl.remove)
|
||||
sessionRoutes.post('/api/hermes/sessions/batch-delete', ctrl.batchRemove)
|
||||
sessionRoutes.post('/api/hermes/sessions/:id/rename', ctrl.rename)
|
||||
sessionRoutes.post('/api/hermes/sessions/:id/workspace', ctrl.setWorkspace)
|
||||
sessionRoutes.get('/api/hermes/workspace/folders', ctrl.listWorkspaceFolders)
|
||||
|
||||
Reference in New Issue
Block a user