feat(web-ui): add pinned sessions and live monitor in Chat (#118)

* feat: add single-page live session monitor and chat pinning

* fix: restore full test green after main merge

* fix: use Array.from instead of Set spread for ts-node compatibility

[...new Set()] requires downlevelIteration which isn't enabled in
ts-node dev mode, causing sonic-boom crash on startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: ekko <fqsy1416@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zhicheng Han
2026-04-22 02:09:58 +02:00
committed by GitHub
parent 83ad9642e2
commit 3f88553765
34 changed files with 2497 additions and 278 deletions
@@ -0,0 +1,58 @@
import { request } from '../client'
export interface ConversationSummary {
id: string
source: string
model: string
title: string | null
started_at: number
ended_at: number | null
last_active: number
message_count: number
tool_call_count: number
input_tokens: number
output_tokens: number
cache_read_tokens: number
cache_write_tokens: number
reasoning_tokens: number
billing_provider: string | null
estimated_cost_usd: number
actual_cost_usd: number | null
cost_status: string
preview: string
is_active: boolean
thread_session_count: number
}
export interface ConversationMessage {
id: number | string
session_id: string
role: 'user' | 'assistant'
content: string
timestamp: number
}
export interface ConversationDetail {
session_id: string
messages: ConversationMessage[]
visible_count: number
thread_session_count: number
}
export async function fetchConversationSummaries(params: { humanOnly?: boolean; source?: string; limit?: number } = {}): Promise<ConversationSummary[]> {
const query = new URLSearchParams()
if (params.humanOnly === false) query.set('humanOnly', 'false')
if (params.source) query.set('source', params.source)
if (params.limit != null) query.set('limit', String(params.limit))
const suffix = query.toString() ? `?${query.toString()}` : ''
const res = await request<{ sessions: ConversationSummary[] }>(`/api/hermes/sessions/conversations${suffix}`)
return res.sessions
}
export async function fetchConversationDetail(sessionId: string, params: { humanOnly?: boolean; source?: string } = {}): Promise<ConversationDetail> {
const query = new URLSearchParams()
if (params.humanOnly === false) query.set('humanOnly', 'false')
if (params.source) query.set('source', params.source)
const suffix = query.toString() ? `?${query.toString()}` : ''
return request<ConversationDetail>(`/api/hermes/sessions/conversations/${encodeURIComponent(sessionId)}/messages${suffix}`)
}