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:
ekko
2026-05-06 16:15:42 +08:00
committed by GitHub
parent d13423b9dd
commit 266f6e1a59
14 changed files with 342 additions and 49 deletions
+33 -36
View File
@@ -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),
}
}
}