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:
@@ -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)!
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user