fix(chat): isolate concurrent session events and workspace dialog i18n (#351)

* feat: per-session workspace with folder picker, HERMES_HOME support, esbuild fix

* fix(chat): isolate concurrent session events and workspace dialog i18n

Two user-visible bugs are fixed here:

1. Workspace dialog title showed the raw i18n key 'chat.setWorkspaceTitle' because the key was never added to en.ts / zh.ts. The dialog is opened from ChatPanel.vue but only 'setWorkspace' existed. Add the missing 'setWorkspaceTitle' translation in both locales.

2. With two concurrent runs the assistant text from session A would show up in session B (and vice versa). The /chat-run namespace uses a single shared Socket.IO connection on the client; every startRunViaSocket() call registers its own listeners on the same socket. The server fans events out via 'session:<id>' rooms, but a single socket can be in multiple rooms at once and there was no per-event filtering on the client. Each run's closure captured its own sid and wrote into the wrong session. The server already tags every payload with session_id, so the fix is a guard inside handleEvent() that drops events whose session_id does not match this run's body.session_id. Untagged events are still accepted for backwards compatibility.

3. Also fix a related crash where setting a workspace on a session that had not been persisted yet (no first message sent) threw because the row did not exist. Create the row on demand inside setWorkspace controller.

* fix: upgrade esbuild to 0.27+ for vite 8 compatibility

---------

Co-authored-by: ekko <fqsy1416@gmail.com>
This commit is contained in:
jsonet
2026-04-30 20:17:38 +08:00
committed by GitHub
parent dac9006b3e
commit 7e7fe90483
14 changed files with 468 additions and 8 deletions
+1
View File
@@ -51,6 +51,7 @@ export const SESSIONS_SCHEMA: Record<string, string> = {
cost_status: 'TEXT NOT NULL DEFAULT \'\'',
preview: 'TEXT NOT NULL DEFAULT \'\'',
last_active: 'INTEGER NOT NULL',
workspace: 'TEXT',
}
export const MESSAGES_TABLE = 'messages'
@@ -29,6 +29,7 @@ export interface HermesSessionRow {
cost_status: string
preview: string
last_active: number
workspace: string | null
}
export interface HermesMessageRow {
@@ -102,6 +103,7 @@ function mapSessionRow(row: Record<string, unknown>): HermesSessionRow {
cost_status: String(row.cost_status || ''),
preview: String(row.preview || ''),
last_active: Number(row.last_active || 0),
workspace: row.workspace != null ? String(row.workspace) : null,
}
}
@@ -131,6 +133,7 @@ export function createSession(data: {
profile?: string
model?: string
title?: string
workspace?: string
}): HermesSessionRow {
const now = Math.floor(Date.now() / 1000)
if (!isSqliteAvailable()) {
@@ -141,14 +144,14 @@ export function createSession(data: {
message_count: 0, tool_call_count: 0,
input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, reasoning_tokens: 0,
billing_provider: null, estimated_cost_usd: 0, actual_cost_usd: null,
cost_status: '', preview: '', last_active: now,
cost_status: '', preview: '', last_active: now, workspace: data.workspace || null,
}
}
const db = getDb()!
db.prepare(
`INSERT INTO ${SESSIONS_TABLE} (id, profile, source, model, title, started_at, last_active)
VALUES (?, ?, 'api_server', ?, ?, ?, ?)`,
).run(data.id, data.profile || 'default', data.model || '', data.title || null, now, now)
`INSERT INTO ${SESSIONS_TABLE} (id, profile, source, model, title, started_at, last_active, workspace)
VALUES (?, ?, 'api_server', ?, ?, ?, ?, ?)`,
).run(data.id, data.profile || 'default', data.model || '', data.title || null, now, now, data.workspace || null)
return getSession(data.id)!
}