feat: add @hypr/helpchat foundation package#4027
feat: add @hypr/helpchat foundation package#4027devin-ai-integration[bot] wants to merge 2 commits intomainfrom
Conversation
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
✅ Deploy Preview for hyprnote canceled.
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
✅ Deploy Preview for hyprnote-storybook canceled.
|
| sourceId: string, | ||
| ): Promise<Conversation[]> { | ||
| const fetchFn = resolveFetch(config); | ||
| const params = new URLSearchParams({ source_id: sourceId }); |
There was a problem hiding this comment.
🔴 Wrong query parameter key source_id should be sourceId for listConversations
The listConversations function sends the query parameter as source_id, but the Rust backend's ListConversationsQuery struct uses #[serde(rename_all = "camelCase")], which means axum's Query extractor (backed by serde_urlencoded) expects the camelCase key sourceId.
Root Cause
In crates/api-support/src/routes/chatwoot/conversation.rs:53-57, the query struct is:
#[derive(Debug, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ListConversationsQuery {
pub source_id: String,
}With rename_all = "camelCase", serde deserializes the field source_id from the key sourceId. However, the TypeScript client at packages/helpchat/src/client.ts:74 sends:
const params = new URLSearchParams({ source_id: sourceId });This sends ?source_id=... in the URL, but the backend expects ?sourceId=.... The request will fail with a deserialization error (likely a 400 or 422 response).
Impact: Every call to listConversations will fail because the backend cannot deserialize the query parameter.
| const params = new URLSearchParams({ source_id: sourceId }); | |
| const params = new URLSearchParams({ sourceId: sourceId }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| sourceId: string, | ||
| ): Promise<Message[]> { | ||
| const fetchFn = resolveFetch(config); | ||
| const params = new URLSearchParams({ source_id: sourceId }); |
There was a problem hiding this comment.
🔴 Wrong query parameter key source_id should be sourceId for getMessages
The getMessages function sends the query parameter as source_id, but the Rust backend reuses ListConversationsQuery (which has #[serde(rename_all = "camelCase")]) for the query extraction, so it expects sourceId.
Root Cause
In crates/api-support/src/routes/chatwoot/message.rs:124, the get_messages handler uses:
axum::extract::Query(params): axum::extract::Query<ListConversationsQuery>,ListConversationsQuery at crates/api-support/src/routes/chatwoot/conversation.rs:53-57 has #[serde(rename_all = "camelCase")], so the query key must be sourceId. But the TypeScript client at packages/helpchat/src/client.ts:118 sends:
const params = new URLSearchParams({ source_id: sourceId });This sends ?source_id=... instead of ?sourceId=..., causing the backend to fail to deserialize the query.
Impact: Every call to getMessages will fail because the backend cannot deserialize the query parameter.
| const params = new URLSearchParams({ source_id: sourceId }); | |
| const params = new URLSearchParams({ sourceId: sourceId }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (payload.content) { | ||
| onMessage(payload); | ||
| } |
There was a problem hiding this comment.
The check if (payload.content) will skip messages with empty string content. Since empty strings are falsy in JavaScript, any AgentMessage with content: "" will be filtered out even though it's a valid message according to the AgentMessage type definition (where content is string, not string | null). This could cause legitimate empty messages from agents to be dropped.
Fix:
if (payload.content !== undefined) {
onMessage(payload);
}Or if empty strings should genuinely be filtered:
if (payload.content && payload.content.trim()) {
onMessage(payload);
}| if (payload.content) { | |
| onMessage(payload); | |
| } | |
| if (payload.content !== undefined) { | |
| onMessage(payload); | |
| } |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
There was a problem hiding this comment.
🚩 Backend inconsistency: query structs use different serde conventions
This is a pre-existing backend issue that directly caused the bugs in this PR. ListConversationsQuery (crates/api-support/src/routes/chatwoot/conversation.rs:54) has #[serde(rename_all = "camelCase")] meaning it expects sourceId in query params, while ConversationEventsQuery (crates/api-support/src/routes/chatwoot/events.rs:12-15) has no rename attribute and expects pubsub_token. This inconsistency is confusing — query parameters are conventionally snake_case or flat lowercase. It may be worth standardizing the backend query structs to not use camelCase rename (or at least documenting the convention), since the utoipa params() annotations at crates/api-support/src/routes/chatwoot/conversation.rs:69 still document the param as source_id in the OpenAPI spec, which further misleads consumers.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it. |
feat: add @hypr/helpchat foundation package
Summary
Adds a new
@hypr/helpchatpackage underpackages/helpchat/— a platform-agnostic TypeScript client for the Chatwoot proxy routes incrates/api-support/src/routes/chatwoot/. This is a foundation-only package: types, API client, and SSE events consumer. No React hooks, no integration with desktop or web apps.This is a cleaner, smaller-scope redo of #4022. Key differences from that PR:
@hypr/api-clientdependency — self-contained with plainfetchfetchFnforwarding — used in both REST calls and SSE (the old PR had a bug wherefetchFnwas only used for SSE)Package contents:
types.ts— Request/response types mirroring the Rust backend shapesclient.ts— API functions:createContact,createConversation,listConversations,sendMessage,getMessagesevents.ts—connectEventStreamfor real-time human agent messages via SSEindex.ts— Barrel exportsReview & Testing Checklist for Human
types.tsare hand-written to mirrorcrates/api-support/src/routes/chatwoot/*.rs(which use#[serde(rename_all = "camelCase")]). Spot-check that field names align (e.g.sourceId,pubsubToken,messageType,conversationId).listConversationsandgetMessagesusesource_id,eventsusespubsub_tokenas query param keys. These should match the Rust serde defaults (snake_case for query deserialization).Notes
pnpm -F @hypr/helpchat typecheckpasses)HelpChatConfig.fetchFnoption exists so Tauri's custom HTTP plugin can be injected later when desktop integration happensLink to Devin run: https://app.devin.ai/sessions/b45858050d7846779f54fb163bb8ed14
Requested by: @yujonglee