mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add session management (rename, archive, delete)
Implements comprehensive session lifecycle management with user-friendly interactions: Rename: Update session metadata.name via PATCH endpoint with conflict detection Archive: Abort active sessions via DELETE endpoint with validation Delete: Permanently remove inactive sessions with cascade cleanup Backend: - Store.deleteSession() removes session and cascade-deletes messages - SyncEngine.renameSession() with concurrency error handling - SyncEngine.deleteSession() with active session validation - PATCH /sessions/:id for rename, DELETE /sessions/:id for delete Frontend Components: - RenameSessionDialog: Text input with auto-focus and error display - SessionActionMenu: Modal with rename, archive, delete buttons - ConfirmDialog: Reusable confirmation with error feedback - SessionHeader: Menu button (⋮) triggering action menu - SessionList: Long-press detection triggering item actions Interactions: - Long-press on session list items (500ms threshold) opens action menu - Menu button in session header (non-Telegram environments only) - Confirmation dialogs with descriptive warnings for destructive actions - Real-time error display in dialogs on operation failure - Haptic feedback on long-press via usePlatform hook Accessibility: - Keyboard support (Enter/Space) for long-press handler - Focus management in RenameSessionDialog - Proper ARIA labels and semantic HTML
This commit is contained in:
@@ -743,4 +743,16 @@ export class Store {
|
||||
).run(platform, platformUserId)
|
||||
return result.changes > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session and all associated data.
|
||||
* Messages are automatically cascade-deleted via foreign key constraint.
|
||||
* Todos are stored in the sessions.todos column and deleted with the row.
|
||||
*/
|
||||
deleteSession(id: string, namespace: string): boolean {
|
||||
const result = this.db.prepare(
|
||||
'DELETE FROM sessions WHERE id = ? AND namespace = ?'
|
||||
).run(id, namespace)
|
||||
return result.changes > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,6 +751,56 @@ export class SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
async renameSession(sessionId: string, name: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) {
|
||||
throw new Error('Session not found')
|
||||
}
|
||||
|
||||
const currentMetadata = session.metadata ?? { path: '', host: '' }
|
||||
const newMetadata = { ...currentMetadata, name }
|
||||
|
||||
const result = this.store.updateSessionMetadata(
|
||||
sessionId,
|
||||
newMetadata,
|
||||
session.metadataVersion,
|
||||
session.namespace
|
||||
)
|
||||
|
||||
if (result.result === 'error') {
|
||||
throw new Error('Failed to update session metadata')
|
||||
}
|
||||
|
||||
if (result.result === 'version-mismatch') {
|
||||
throw new Error('Session was modified concurrently. Please try again.')
|
||||
}
|
||||
|
||||
this.refreshSession(sessionId)
|
||||
}
|
||||
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) {
|
||||
throw new Error('Session not found')
|
||||
}
|
||||
|
||||
if (session.active) {
|
||||
throw new Error('Cannot delete active session')
|
||||
}
|
||||
|
||||
const deleted = this.store.deleteSession(sessionId, session.namespace)
|
||||
if (!deleted) {
|
||||
throw new Error('Failed to delete session')
|
||||
}
|
||||
|
||||
this.sessions.delete(sessionId)
|
||||
this.sessionMessages.delete(sessionId)
|
||||
this.lastBroadcastAtBySessionId.delete(sessionId)
|
||||
this.todoBackfillAttemptedSessionIds.delete(sessionId)
|
||||
|
||||
this.emit({ type: 'session-removed', sessionId, namespace: session.namespace })
|
||||
}
|
||||
|
||||
async applySessionConfig(
|
||||
sessionId: string,
|
||||
config: {
|
||||
|
||||
@@ -67,6 +67,10 @@ const modelModeSchema = z.object({
|
||||
model: z.enum(['default', 'sonnet', 'opus'])
|
||||
})
|
||||
|
||||
const renameSessionSchema = z.object({
|
||||
name: z.string().min(1).max(255)
|
||||
})
|
||||
|
||||
export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Hono<WebAppEnv> {
|
||||
const app = new Hono<WebAppEnv>()
|
||||
|
||||
@@ -213,6 +217,64 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
|
||||
}
|
||||
})
|
||||
|
||||
app.patch('/sessions/:id', async (c) => {
|
||||
const engine = requireSyncEngine(c, getSyncEngine)
|
||||
if (engine instanceof Response) {
|
||||
return engine
|
||||
}
|
||||
|
||||
const sessionResult = requireSessionFromParam(c, engine)
|
||||
if (sessionResult instanceof Response) {
|
||||
return sessionResult
|
||||
}
|
||||
|
||||
const body = await c.req.json().catch(() => null)
|
||||
const parsed = renameSessionSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: 'Invalid body: name is required' }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
await engine.renameSession(sessionResult.sessionId, parsed.data.name)
|
||||
return c.json({ ok: true })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to rename session'
|
||||
// Map concurrency/version errors to 409 conflict
|
||||
if (message.includes('concurrently') || message.includes('version')) {
|
||||
return c.json({ error: message }, 409)
|
||||
}
|
||||
return c.json({ error: message }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.delete('/sessions/:id', async (c) => {
|
||||
const engine = requireSyncEngine(c, getSyncEngine)
|
||||
if (engine instanceof Response) {
|
||||
return engine
|
||||
}
|
||||
|
||||
const sessionResult = requireSessionFromParam(c, engine)
|
||||
if (sessionResult instanceof Response) {
|
||||
return sessionResult
|
||||
}
|
||||
|
||||
if (sessionResult.session.active) {
|
||||
return c.json({ error: 'Cannot delete active session. Archive it first.' }, 409)
|
||||
}
|
||||
|
||||
try {
|
||||
await engine.deleteSession(sessionResult.sessionId)
|
||||
return c.json({ ok: true })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete session'
|
||||
// Map "active session" error to 409 conflict (race condition: session became active)
|
||||
if (message.includes('active')) {
|
||||
return c.json({ error: message }, 409)
|
||||
}
|
||||
return c.json({ error: message }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/sessions/:id/slash-commands', async (c) => {
|
||||
const engine = requireSyncEngine(c, getSyncEngine)
|
||||
if (engine instanceof Response) {
|
||||
|
||||
Reference in New Issue
Block a user