Skip to content

feat: add @hypr/helpchat foundation package#4027

Open
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
devin/helpchat-foundation
Open

feat: add @hypr/helpchat foundation package#4027
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
devin/helpchat-foundation

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 17, 2026

feat: add @hypr/helpchat foundation package

Summary

Adds a new @hypr/helpchat package under packages/helpchat/ — a platform-agnostic TypeScript client for the Chatwoot proxy routes in crates/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:

  • No React hooks or desktop/web integration — just the client foundation
  • No @hypr/api-client dependency — self-contained with plain fetch
  • Consistent fetchFn forwarding — used in both REST calls and SSE (the old PR had a bug where fetchFn was only used for SSE)
  • Zero changes to existing code — purely additive

Package contents:

  • types.ts — Request/response types mirroring the Rust backend shapes
  • client.ts — API functions: createContact, createConversation, listConversations, sendMessage, getMessages
  • events.tsconnectEventStream for real-time human agent messages via SSE
  • index.ts — Barrel exports

Review & Testing Checklist for Human

⚠️ Risk Level: GREEN — Additive-only, no existing code changed, no consumers yet.

  • Verify TS types match Rust backend shapes — Types in types.ts are hand-written to mirror crates/api-support/src/routes/chatwoot/*.rs (which use #[serde(rename_all = "camelCase")]). Spot-check that field names align (e.g. sourceId, pubsubToken, messageType, conversationId).
  • Check query param keyslistConversations and getMessages use source_id, events uses pubsub_token as query param keys. These should match the Rust serde defaults (snake_case for query deserialization).

Notes

  • Package typechecks clean (pnpm -F @hypr/helpchat typecheck passes)
  • No runtime tests — this is intentionally foundation-only with no consumers yet
  • The HelpChatConfig.fetchFn option exists so Tauri's custom HTTP plugin can be injected later when desktop integration happens

Link to Devin run: https://app.devin.ai/sessions/b45858050d7846779f54fb163bb8ed14
Requested by: @yujonglee


Open with Devin

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@netlify
Copy link

netlify bot commented Feb 17, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit 8640b13
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/699416809779010008ca0d8a

@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Feb 17, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit 8640b13
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/699416800873ec00081cc43b

Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

sourceId: string,
): Promise<Conversation[]> {
const fetchFn = resolveFetch(config);
const params = new URLSearchParams({ source_id: sourceId });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
const params = new URLSearchParams({ source_id: sourceId });
const params = new URLSearchParams({ sourceId: sourceId });
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

sourceId: string,
): Promise<Message[]> {
const fetchFn = resolveFetch(config);
const params = new URLSearchParams({ source_id: sourceId });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
const params = new URLSearchParams({ source_id: sourceId });
const params = new URLSearchParams({ sourceId: sourceId });
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +61 to +63
if (payload.content) {
onMessage(payload);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
}
Suggested change
if (payload.content) {
onMessage(payload);
}
if (payload.content !== undefined) {
onMessage(payload);
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration
Copy link
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments