diff --git a/docs/chat-chain-changes/2026-10-24-profile-startup.md b/docs/chat-chain-changes/2026-10-24-profile-startup.md new file mode 100644 index 0000000..01f1c12 --- /dev/null +++ b/docs/chat-chain-changes/2026-10-24-profile-startup.md @@ -0,0 +1,14 @@ +--- +date: 2026-10-24 +pr: pending +feature: Conversation startup and profile loading +impact: Load conversations independently of metadata and recover failed resume requests via HTTP. +--- + +# 2026-10-24: Conversation startup and profile loading + +- PR/commit: pending local review. +- Touched feature: client chat startup, session switching, resume listener lifecycle, and profile list metadata. +- Behavior impact: conversation loading no longer waits for profile/settings/model metadata. Session switches ignore obsolete replies (including A/B/A), cancel obsolete resume listeners, and fall back to paginated HTTP history after a three-second resume timeout. Failed history loads show a retry action. Profile inventory uses disk metadata instead of spawning Hermes CLI; uploaded avatar bodies are fetched on demand through an access-checked endpoint. +- The `/chat-run` event names, fields and server emission timing remain unchanged. Only the client's wait/fallback behavior changes. +- Validation: focused store/view/profile tests, Playwright startup/retry/multi-tab/profile tests, full Vitest coverage, harness and production build. See task results for actual outcomes. diff --git a/docs/openapi.json b/docs/openapi.json index 0f458c5..24fdae5 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -7375,6 +7375,40 @@ } }, "/api/hermes/profiles/{name}/avatar": { + "get": { + "tags": [ + "Profiles" + ], + "summary": "Get avatar", + "description": "GET /api/hermes/profiles/:name/avatar", + "operationId": "getAvatar", + "security": [ + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "description": "Not found" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, "put": { "tags": [ "Profiles" diff --git a/packages/client/src/api/hermes/chat.ts b/packages/client/src/api/hermes/chat.ts index f48df7d..701d9ae 100644 --- a/packages/client/src/api/hermes/chat.ts +++ b/packages/client/src/api/hermes/chat.ts @@ -788,14 +788,21 @@ export function resumeSession( onResumed: (data: ResumeSessionPayload) => void, profile?: string | null, transport: ChatRunTransport = 'chat-run', + signal?: AbortSignal, ): Socket { const socket = connectChatRun(profile, transport) const handleResumed = (data: ResumeSessionPayload) => { if (data?.session_id !== sessionId) return - removeSocketListener(socket, 'resumed', handleResumed) + cleanup() onResumed(data) } + const cleanup = () => { + removeSocketListener(socket, 'resumed', handleResumed) + signal?.removeEventListener('abort', cleanup) + } + if (signal?.aborted) return socket + signal?.addEventListener('abort', cleanup, { once: true }) socket.on('resumed', handleResumed) socket.emit('resume', { session_id: sessionId, ...(profile ? { profile } : {}) }) diff --git a/packages/client/src/api/hermes/profiles.ts b/packages/client/src/api/hermes/profiles.ts index e8fbe00..c7d877f 100644 --- a/packages/client/src/api/hermes/profiles.ts +++ b/packages/client/src/api/hermes/profiles.ts @@ -242,3 +242,8 @@ export async function importProfile(file: File): Promise { return false } } + +export async function fetchProfileAvatar(name: string): Promise { + const res = await request<{ avatar: ProfileAvatar | null }>(`/api/hermes/profiles/${encodeURIComponent(name)}/avatar`) + return res.avatar +} diff --git a/packages/client/src/components/hermes/chat/MessageList.vue b/packages/client/src/components/hermes/chat/MessageList.vue index 299d9f8..e55a8a5 100644 --- a/packages/client/src/components/hermes/chat/MessageList.vue +++ b/packages/client/src/components/hermes/chat/MessageList.vue @@ -450,6 +450,11 @@ defineExpose({