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
+21 -5
View File
@@ -13,10 +13,23 @@ const mockChatStore = vi.hoisted(() => ({
deleteSession: vi.fn(),
}))
const mockPrefsStore = vi.hoisted(() => ({
pinnedIds: [] as string[],
humanOnly: true,
isPinned: vi.fn(() => false),
togglePinned: vi.fn(),
setHumanOnly: vi.fn(),
pruneMissingSessions: vi.fn(),
}))
vi.mock('@/stores/hermes/chat', () => ({
useChatStore: () => mockChatStore,
}))
vi.mock('@/stores/hermes/session-browser-prefs', () => ({
useSessionBrowserPrefsStore: () => mockPrefsStore,
}))
vi.mock('@/api/hermes/sessions', () => ({
renameSession: vi.fn(),
}))
@@ -33,6 +46,12 @@ vi.mock('@/components/hermes/chat/ChatInput.vue', () => ({
},
}))
vi.mock('@/components/hermes/chat/ConversationMonitorPane.vue', () => ({
default: {
template: '<div class="conversation-monitor-mock" />',
},
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string) => key,
@@ -128,8 +147,8 @@ describe('ChatPanel session list', () => {
const sessionTitles = wrapper.findAll('.session-item-title').map(node => node.text())
expect(sessionTitles.slice(0, 2)).toEqual(['Discord Active', 'Discord Older'])
const activeIndicator = wrapper.find('.session-item.active .session-item-active-indicator')
expect(activeIndicator.exists()).toBe(true)
const liveRow = wrapper.findAll('.session-item').find(node => node.text().includes('Discord Active'))
expect(liveRow?.find('.session-item-active-indicator').exists()).toBe(true)
await wrapper.findAll('.session-item').find(node => node.text().includes('Slack Selected'))!.trigger('click')
@@ -137,8 +156,5 @@ describe('ChatPanel session list', () => {
const groupLabelsAfterClick = wrapper.findAll('.session-group-label').map(node => node.text())
expect(groupLabelsAfterClick[0]).toBe('Discord')
const activeTitlesAfterClick = wrapper.findAll('.session-item.active .session-item-title').map(node => node.text())
expect(activeTitlesAfterClick).toEqual(['Discord Active'])
})
})