Skip to content

feat: local model auto-reaction on user messages by channel type#181

Merged
senamakel merged 7 commits into
mainfrom
feat/message-reactions
Apr 1, 2026
Merged

feat: local model auto-reaction on user messages by channel type#181
senamakel merged 7 commits into
mainfrom
feat/message-reactions

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented Apr 1, 2026

Summary

  • Adds a local AI operation (local_ai_should_react) that uses the Ollama model to decide whether to auto-react to a user's message with an emoji, based on channel type
  • Casual channels (discord, telegram) get more frequent/playful reactions; professional channels (web, slack, email) are more reserved
  • Integrated into the Conversations page as fire-and-forget — zero cloud cost, no blocking

Changes

  • Rust (src/openhuman/local_ai/ops.rs): new local_ai_should_react operation with ReactionDecision return type, emoji extraction helpers, and unit tests
  • Rust (src/openhuman/local_ai/schemas.rs): registered controller schema and handler for local_ai_should_react
  • TypeScript (app/src/utils/tauriCommands.ts): new openhumanLocalAiShouldReact() binding
  • React (app/src/pages/Conversations.tsx): maybeAutoReact() helper called after both local and cloud response paths

Test plan

  • Rust unit tests for emoji extraction (cargo test -- local_ai::ops)
  • cargo check passes clean
  • tsc --noEmit passes clean
  • Prettier + ESLint pass
  • Manual: send a message on a telegram-connected thread → verify emoji reaction appears on user message
  • Manual: send a message on web channel → verify reactions are less frequent
  • Manual: verify no reaction when local model is not ready

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Automatic emoji reactions from a local AI model: user messages may receive context-aware emoji replies when a local model is active.
  • Bug Fixes
    • Reactions now display for any sender when present; manual reaction picker remains available only for agent messages.
    • Improved reaction reliability to prevent duplicate or stale reactions and ensure timely reaction delivery.

senamakel and others added 3 commits April 1, 2026 15:02
- Added functionality to automatically react to user messages with emojis based on the local AI's evaluation of the message content and channel type.
- Introduced `maybeAutoReact` function to handle the decision-making process for emoji reactions.
- Updated the `Conversations` component to store the last user message and trigger reactions accordingly.
- Enhanced the `tauriCommands` with a new method `openhumanLocalAiShouldReact` to facilitate local model evaluations for reactions.
- Updated local AI operations to include reaction decision logic, ensuring efficient and context-aware responses.

This feature enhances user engagement by adding a personal touch to interactions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 437eb829-d247-4283-bb6b-ec7397643bc8

📥 Commits

Reviewing files that changed from the base of the PR and between f96036d and 1144c96.

📒 Files selected for processing (2)
  • app/src/pages/Conversations.tsx
  • app/src/utils/tauriCommands.ts

📝 Walkthrough

Walkthrough

Adds an automatic emoji reaction flow: frontend tracks pending messages and calls a new Tauri RPC; Tauri forwards to a newly-registered backend controller that prompts the local AI, parses an emoji decision, and returns a ReactionDecision used to dispatch addReaction.

Changes

Cohort / File(s) Summary
Frontend Conversations
app/src/pages/Conversations.tsx
Adds defaultChannelType, pendingReactionRef, and maybeAutoReact; schedules/consumes pending auto-reactions on local/cloud delivery or error; adjusts reaction UI rendering and reaction picker availability.
Tauri Command Interface
app/src/utils/tauriCommands.ts
Adds ReactionDecision interface and openhumanLocalAiShouldReact(message, channelType) wrapper calling core RPC openhuman.local_ai_should_react.
Backend Local AI Ops
src/openhuman/local_ai/ops.rs
Introduces ReactionDecision struct and local_ai_should_react async op: short-circuits on empty/not-ready, prompts model with a constrained instruction, extracts first emoji (helpers included), and returns decision; includes unit tests.
Backend Controller & Schemas
src/openhuman/local_ai/schemas.rs
Adds LocalAiShouldReactParams, registers local_ai_should_react schema/controller, implements handle_local_ai_should_react which loads config, calls the op, and returns JSON decision.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as Conversations.tsx
    participant Tauri as Tauri Bridge
    participant Backend as openhuman Backend
    participant LocalAI as Local AI Model

    User->>Frontend: send message
    Frontend->>Frontend: addMessageLocal() + set pendingReactionRef
    alt local-model branch
        Frontend->>Frontend: local-model delivers -> clear pending & call maybeAutoReact
    else cloud branch
        Frontend->>Tauri: cloud onDone -> consumes pending and calls maybeAutoReact
    end
    Frontend->>Tauri: openhumanLocalAiShouldReact(message, channelType)
    Tauri->>Backend: RPC openhuman.local_ai_should_react(params)
    Backend->>LocalAI: prompt for emoji decision
    LocalAI-->>Backend: model response (emoji or NONE)
    Backend->>Backend: extract_first_emoji() -> ReactionDecision
    Backend-->>Tauri: CommandResponse<ReactionDecision>
    Tauri-->>Frontend: { should_react, emoji }
    alt should_react == true
        Frontend->>Frontend: dispatch addReaction(msgId, emoji)
    else
        Frontend->>Frontend: clear pendingReactionRef
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A rabbit sniffs the message, whiskers poised to pick,
"Is it NONE or an emoji?" it asks the little trick.
If the model nods yes, a tiny symbol hops in place,
Silent as soft fur, a pixelated embrace.
Hooray for tiny reactions—hop, tap, and grace! 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: introducing local model auto-reactions triggered by user messages, with behavior varied by channel type. It is concise, specific, and clearly summarizes the primary feature addition.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/message-reactions

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/src/utils/tauriCommands.ts (1)

1376-1396: Please extract this new wrapper out of tauriCommands.ts.

This file is already ~2k lines, so adding another feature-specific local-AI type/command here makes the module harder to navigate and keeps pushing past the repo’s ~500-line cap. A dedicated localAiCommands.ts or localAiReactions.ts module would keep this surface maintainable.

As per coding guidelines "Keep source files at ≤ ~500 lines per file; split modules when growing larger".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/utils/tauriCommands.ts` around lines 1376 - 1396, The new local-AI
wrapper (ReactionDecision interface and openhumanLocalAiShouldReact function)
should be moved out of tauriCommands.ts into its own module (e.g.,
localAiCommands.ts or localAiReactions.ts); create the new file, paste the
ReactionDecision type and the openhumanLocalAiShouldReact implementation there,
export them, and replace the original definitions in tauriCommands.ts with an
import of those exports; also update any call sites to import
openhumanLocalAiShouldReact and/or ReactionDecision from the new module so
behavior is unchanged and tauriCommands.ts stays under the ~500-line guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/pages/Conversations.tsx`:
- Line 131: lastUserMessageRef currently holds a single {id, content, threadId}
and races across retries/cancels; replace it with a per-request correlation
token so onDone only applies to the matching round. Concretely: generate a
unique token (e.g., UUID or incrementing requestId) for each send/round, store
the token and message info in a Map or attach it to the request object instead
of using lastUserMessageRef, pass that token into the onDone callback and have
onDone verify the token matches the current entry before applying/clearing
reactions; update all usages that read/write lastUserMessageRef (including the
onDone handlers referenced) to use the per-request map/token check. Ensure
cancellation/retry updates the current token so stale onDone invocations are
ignored.
- Around line 493-499: The auto-reactions dispatched by
maybeAutoReact/openhumanLocalAiShouldReact are being added to userMessageId but
the Conversations component only renders reactions for agent messages (see
myReactions rendered inside the msg.sender === 'agent' branch), so
auto-reactions are invisible; fix by either rendering myReactions for user
bubbles (add the same myReactions render logic into the user msg.sender !==
'agent' branch) or change maybeAutoReact to target the assistant message instead
(find the corresponding assistant messageId in the same thread/turn and dispatch
addReaction({ threadId, messageId: assistantMessageId, emoji }) rather than
using userMessageId); update references to addReaction, userMessageId, threadId,
maybeAutoReact/openhumanLocalAiShouldReact and the msg.sender === 'agent' render
branch accordingly.

In `@src/openhuman/local_ai/ops.rs`:
- Around line 588-607: The extractor extract_first_emoji currently splits flag
emojis because it only treats additional emoji scalars after a ZWJ as part of
the same emoji; update extract_first_emoji to detect regional indicator symbols
(U+1F1E6..U+1F1FF) and, when the first scalar is a regional indicator, consume
any immediately consecutive regional indicator scalars to form a single emoji
token (do this same fix where the other emoji-scanning logic exists around the
645-676 region). Also add a regression unit test that calls
extract_first_emoji("🇺🇸") and asserts the returned Some("🇺🇸") to prevent
regressions.
- Around line 543-546: The debug call in should_react currently logs the model's
raw output via tracing::debug!(raw_output = trimmed, ...), which can persist
user PII; change the tracing call to avoid logging verbatim model
responses—either remove the raw_output field altogether or log a
redacted/hashed/length-only representation (e.g., first N characters +
"[REDACTED]" or a hash) and keep the same context string; update the
tracing::debug invocation in the should_react path (the lines using
tracing::debug and variable trimmed) accordingly.

---

Nitpick comments:
In `@app/src/utils/tauriCommands.ts`:
- Around line 1376-1396: The new local-AI wrapper (ReactionDecision interface
and openhumanLocalAiShouldReact function) should be moved out of
tauriCommands.ts into its own module (e.g., localAiCommands.ts or
localAiReactions.ts); create the new file, paste the ReactionDecision type and
the openhumanLocalAiShouldReact implementation there, export them, and replace
the original definitions in tauriCommands.ts with an import of those exports;
also update any call sites to import openhumanLocalAiShouldReact and/or
ReactionDecision from the new module so behavior is unchanged and
tauriCommands.ts stays under the ~500-line guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bb69cf8-382c-431a-9fdc-7945a763a43c

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce9011 and 9ec2b88.

📒 Files selected for processing (4)
  • app/src/pages/Conversations.tsx
  • app/src/utils/tauriCommands.ts
  • src/openhuman/local_ai/ops.rs
  • src/openhuman/local_ai/schemas.rs

Comment thread app/src/pages/Conversations.tsx Outdated
Comment thread app/src/pages/Conversations.tsx
Comment thread src/openhuman/local_ai/ops.rs
Comment thread src/openhuman/local_ai/ops.rs
senamakel and others added 4 commits April 1, 2026 15:47
…oji, PII log

- Replace lastUserMessageRef with per-thread pendingReactionRef Map so
  cancellation/retry clears stale entries and onDone only applies to
  the matching request round
- Render reactions on user message bubbles (not just agent messages)
  so auto-reactions are actually visible; manual picker stays agent-only
- Fix extract_first_emoji to consume consecutive regional indicator
  symbols as a single flag emoji (e.g. 🇺🇸); add regression test
- Replace raw_output in should_react debug log with output_len to
  avoid persisting user PII in traces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion and TTS

- Cleaned up the Conversations component by removing unused imports related to local AI transcription and text-to-speech functionalities, streamlining the codebase and improving maintainability.es
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
app/src/pages/Conversations.tsx (1)

131-133: ⚠️ Potential issue | 🟠 Major

Per-thread pending entries still race within the same thread.

Keying the map by threadId fixes cross-thread overlap, but a late onDone from an earlier cancelled/retried round can still consume the newer entry for that thread and react to the wrong message. This still needs a per-request/round token that onDone validates before applying or clearing the pending entry.

Also applies to: 359-364, 537-541

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/pages/Conversations.tsx` around lines 131 - 133, pendingReactionRef
keyed by threadId still allows races within a thread; modify the pending entry
shape stored in pendingReactionRef to include a per-request token (e.g., roundId
or requestId) when you set a pending reaction in the functions that create
entries, then have onDone validate that token before applying or clearing the
entry. Specifically, update the Map value type used by pendingReactionRef to
include a token, generate a unique token when creating the pending entry (in the
functions that currently set msgId/content for a thread), attach that token to
responses/promises, and in onDone compare the provided token to the token
currently in pendingReactionRef for the threadId—only proceed to react or clear
if they match; apply the same token-check pattern to the other occurrences
referenced (the other pending-reaction blocks).
🧹 Nitpick comments (2)
app/src/pages/Conversations.tsx (1)

491-509: Extract the auto-reaction flow out of Conversations.

This adds request bookkeeping, RPC coordination, and extra render logic to a component that is already very large. A small useAutoReaction-style hook/helper would make the send lifecycle easier to test and keep this file from growing further. As per coding guidelines "Keep source files at ≤ ~500 lines per file; split modules when growing larger".

Also applies to: 537-541, 912-978

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/pages/Conversations.tsx` around lines 491 - 509, Extract the
auto-reaction flow into a dedicated hook (e.g., useAutoReaction) to remove
request bookkeeping, RPC coordination, and render logic from Conversations: move
logic currently in maybeAutoReact that calls openhumanLocalAiShouldReact and
dispatches addReaction (and checks isTauri/isLocalModelActiveRef) into the new
hook, expose a simple API like triggerAutoReact(threadId, messageId,
messageContent) that handles promise lifecycle, deduplication/bookkeeping, and
error logging, and replace the inline calls in Conversations (including other
occurrences around lines 537-541 and 912-978) with calls to this hook to keep
the component small and testable.
src/openhuman/local_ai/ops.rs (1)

483-585: Split the reaction-decision code out of ops.rs.

This prompt/parser/test bundle is a clean standalone slice of functionality, and ops.rs is already well past the repo’s file-size target. Moving it into a dedicated local-AI reactions module would keep the controller surface easier to navigate and test. As per coding guidelines "Keep source files at ≤ ~500 lines per file; split modules when growing larger".

Also applies to: 587-704

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/local_ai/ops.rs` around lines 483 - 585, The reaction decision
logic (ReactionDecision struct and local_ai_should_react function, plus
extract_first_emoji usage and its tests) should be moved out of ops.rs into a
new module (e.g., local_ai::reactions) to keep ops.rs small: create a new
file/module containing the ReactionDecision definition, the
local_ai_should_react async function (preserving the prompt, service usage, and
error handling), and any unit tests that exercise this behavior; update mod
declarations and add the necessary use imports (RpcOutcome, Config,
local_ai::global, extract_first_emoji or move its implementation alongside if
tightly coupled), then replace the original code in ops.rs with a concise call
to local_ai::reactions::local_ai_should_react (and apply the same split for the
related block referenced at 587-704). Ensure public visibility where needed and
run tests to confirm no missing imports or visibility issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/pages/Conversations.tsx`:
- Around line 128-130: The socket callback captures a stale defaultChannelType
because subscribeChatEvents only re-subscribes on rustChat/socketStatus; update
the effect so maybeAutoReact sees current defaultChannelType by either adding
defaultChannelType (from useAppSelector) to the subscribeChatEvents effect
dependency array or by reading defaultChannelType from a ref inside
maybeAutoReact (create a ref updated on selector changes and reference
ref.current in maybeAutoReact). Ensure the change targets the
subscribeChatEvents effect and the maybeAutoReact/onDone callback so
auto-reactions always use the latest defaultChannelType.

In `@src/openhuman/local_ai/ops.rs`:
- Around line 529-538: The prompt builder currently uses the full user message
variable when constructing the reaction prompt (the prompt string and call to
service.prompt in ops.rs), which can feed very large content into Ollama;
truncate the message to a small character budget (e.g., 256–512 chars) before
formatting the prompt—create or use a helper like
truncate_message(original_message, max_chars) that produces a shorter string
(append an ellipsis when truncated) and pass that truncated value into format!
so the prompt and subsequent service.prompt(config, &prompt, ...) call only
include the bounded excerpt.
- Around line 589-604: extract_first_emoji currently consumes all consecutive
regional indicators, so adjacent flags like "🇺🇸🇨🇦" are returned as one
reaction; change the regional-indicator handling in extract_first_emoji to
collect at most two regional indicators (a single pair) and then stop/return
immediately (i.e., if the first char is a regional indicator, optionally consume
one more regional indicator to form the flag and return that string), and add a
regression test asserting extract_first_emoji("🇺🇸🇨🇦") ==
Some("🇺🇸".to_string()); apply the same one-or-two regional-indicator limit to
the other similar regional-indicator handling block in this file.

---

Duplicate comments:
In `@app/src/pages/Conversations.tsx`:
- Around line 131-133: pendingReactionRef keyed by threadId still allows races
within a thread; modify the pending entry shape stored in pendingReactionRef to
include a per-request token (e.g., roundId or requestId) when you set a pending
reaction in the functions that create entries, then have onDone validate that
token before applying or clearing the entry. Specifically, update the Map value
type used by pendingReactionRef to include a token, generate a unique token when
creating the pending entry (in the functions that currently set msgId/content
for a thread), attach that token to responses/promises, and in onDone compare
the provided token to the token currently in pendingReactionRef for the
threadId—only proceed to react or clear if they match; apply the same
token-check pattern to the other occurrences referenced (the other
pending-reaction blocks).

---

Nitpick comments:
In `@app/src/pages/Conversations.tsx`:
- Around line 491-509: Extract the auto-reaction flow into a dedicated hook
(e.g., useAutoReaction) to remove request bookkeeping, RPC coordination, and
render logic from Conversations: move logic currently in maybeAutoReact that
calls openhumanLocalAiShouldReact and dispatches addReaction (and checks
isTauri/isLocalModelActiveRef) into the new hook, expose a simple API like
triggerAutoReact(threadId, messageId, messageContent) that handles promise
lifecycle, deduplication/bookkeeping, and error logging, and replace the inline
calls in Conversations (including other occurrences around lines 537-541 and
912-978) with calls to this hook to keep the component small and testable.

In `@src/openhuman/local_ai/ops.rs`:
- Around line 483-585: The reaction decision logic (ReactionDecision struct and
local_ai_should_react function, plus extract_first_emoji usage and its tests)
should be moved out of ops.rs into a new module (e.g., local_ai::reactions) to
keep ops.rs small: create a new file/module containing the ReactionDecision
definition, the local_ai_should_react async function (preserving the prompt,
service usage, and error handling), and any unit tests that exercise this
behavior; update mod declarations and add the necessary use imports (RpcOutcome,
Config, local_ai::global, extract_first_emoji or move its implementation
alongside if tightly coupled), then replace the original code in ops.rs with a
concise call to local_ai::reactions::local_ai_should_react (and apply the same
split for the related block referenced at 587-704). Ensure public visibility
where needed and run tests to confirm no missing imports or visibility issues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da59ee5f-a611-4de9-a6da-2df54338db2d

📥 Commits

Reviewing files that changed from the base of the PR and between 9ec2b88 and f96036d.

📒 Files selected for processing (2)
  • app/src/pages/Conversations.tsx
  • src/openhuman/local_ai/ops.rs

Comment on lines +128 to +130
const defaultChannelType = useAppSelector(
state => state.channelConnections?.defaultMessagingChannel ?? 'web'
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

wc -l app/src/pages/Conversations.tsx

Repository: tinyhumansai/openhuman

Length of output: 102


🏁 Script executed:

cat -n app/src/pages/Conversations.tsx | sed -n '120,140p'

Repository: tinyhumansai/openhuman

Length of output: 1065


🏁 Script executed:

# Find the subscribeChatEvents effect to understand its dependencies
rg -n "subscribeChatEvents" app/src/pages/Conversations.tsx -A 20 -B 5

Repository: tinyhumansai/openhuman

Length of output: 1795


🏁 Script executed:

# Also check around line 498-499 mentioned
cat -n app/src/pages/Conversations.tsx | sed -n '490,510p'

Repository: tinyhumansai/openhuman

Length of output: 862


🏁 Script executed:

# Look for onDone callback usage
rg -n "onDone" app/src/pages/Conversations.tsx -A 5 -B 5

Repository: tinyhumansai/openhuman

Length of output: 1122


🏁 Script executed:

# Get the full subscribeChatEvents effect and its dependency array
cat -n app/src/pages/Conversations.tsx | sed -n '320,450p'

Repository: tinyhumansai/openhuman

Length of output: 5630


🏁 Script executed:

# Search specifically for defaultChannelType usage in the file
rg -n "defaultChannelType" app/src/pages/Conversations.tsx -A 3 -B 3

Repository: tinyhumansai/openhuman

Length of output: 828


🏁 Script executed:

# Get the end of the subscribeChatEvents effect to see the dependency array
cat -n app/src/pages/Conversations.tsx | sed -n '450,470p'

Repository: tinyhumansai/openhuman

Length of output: 987


🏁 Script executed:

# Find the dependency array of the subscribeChatEvents effect
cat -n app/src/pages/Conversations.tsx | sed -n '470,490p'

Repository: tinyhumansai/openhuman

Length of output: 794


🏁 Script executed:

# Check if maybeAutoReact is defined and where it uses defaultChannelType
cat -n app/src/pages/Conversations.tsx | sed -n '523,540p'

Repository: tinyhumansai/openhuman

Length of output: 949


🏁 Script executed:

# Look for other potential uses of defaultChannelType in callbacks
rg -n "defaultChannelType" app/src/pages/Conversations.tsx

Repository: tinyhumansai/openhuman

Length of output: 191


🏁 Script executed:

# Double-check what's actually at lines 498-499 in context
cat -n app/src/pages/Conversations.tsx | sed -n '495,505p'

Repository: tinyhumansai/openhuman

Length of output: 430


defaultChannelType goes stale in the socket callback due to missing dependency.

The subscribeChatEvents effect only re-subscribes on rustChat / socketStatus (line 485), so onDone keeps the maybeAutoReact function from when the effect was created. Since maybeAutoReact reads defaultChannelType directly (line 526), if the user changes the default channel while this page stays mounted, auto-reactions will use the stale channel value until reconnect/remount.

Suggested fix
   const defaultChannelType = useAppSelector(
     state => state.channelConnections?.defaultMessagingChannel ?? 'web'
   );
+  const defaultChannelTypeRef = useRef(defaultChannelType);
   const pendingReactionRef = useRef<
     Map<string, { msgId: string; content: string; threadId: string }>
   >(new Map());

+  useEffect(() => {
+    defaultChannelTypeRef.current = defaultChannelType;
+  }, [defaultChannelType]);
+
   const maybeAutoReact = (userMessageId: string, messageContent: string, threadId: string) => {
     if (!isTauri() || !isLocalModelActiveRef.current) return;

-    void openhumanLocalAiShouldReact(messageContent, defaultChannelType)
+    void openhumanLocalAiShouldReact(messageContent, defaultChannelTypeRef.current)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/pages/Conversations.tsx` around lines 128 - 130, The socket callback
captures a stale defaultChannelType because subscribeChatEvents only
re-subscribes on rustChat/socketStatus; update the effect so maybeAutoReact sees
current defaultChannelType by either adding defaultChannelType (from
useAppSelector) to the subscribeChatEvents effect dependency array or by reading
defaultChannelType from a ref inside maybeAutoReact (create a ref updated on
selector changes and reference ref.current in maybeAutoReact). Ensure the change
targets the subscribeChatEvents effect and the maybeAutoReact/onDone callback so
auto-reactions always use the latest defaultChannelType.

Comment on lines +529 to +538
let prompt = format!(
"You decide whether an AI assistant should react to a user message with a single emoji. \
Consider the channel context: casual channels (discord, telegram) get more frequent \
reactions with playful emojis, while professional channels (web, slack, email) are more \
reserved — only react to clearly emotional or noteworthy messages.\n\n\
Channel: {channel_type}\nUser message: {message}\n\n\
Reply with EXACTLY one word: either NONE (no reaction) or a single emoji character."
);

let output = service.prompt(config, &prompt, Some(8), true).await;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Bound the message excerpt before this reaction prompt.

This path is meant to be lightweight, but it currently feeds the full user message back through Ollama. Large pasted content turns auto-reactions into another full-context inference and can contend with the next local task. Truncate to a small character budget before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/local_ai/ops.rs` around lines 529 - 538, The prompt builder
currently uses the full user message variable when constructing the reaction
prompt (the prompt string and call to service.prompt in ops.rs), which can feed
very large content into Ollama; truncate the message to a small character budget
(e.g., 256–512 chars) before formatting the prompt—create or use a helper like
truncate_message(original_message, max_chars) that produces a shorter string
(append an ellipsis when truncated) and pass that truncated value into format!
so the prompt and subsequent service.prompt(config, &prompt, ...) call only
include the bounded excerpt.

Comment on lines +589 to +604
fn extract_first_emoji(text: &str) -> Option<String> {
let mut chars = text.chars();
while let Some(ch) = chars.next() {
// Regional indicator pair → flag emoji (e.g. 🇺🇸 = U+1F1FA U+1F1F8)
if is_regional_indicator(ch) {
let mut emoji = String::new();
emoji.push(ch);
// Consume consecutive regional indicators (flags are pairs)
for next in chars.by_ref() {
if is_regional_indicator(next) {
emoji.push(next);
} else {
break;
}
}
return Some(emoji);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Adjacent flags are still parsed as one reaction.

extract_first_emoji now keeps a flag pair together, but it also keeps consuming every following regional indicator. extract_first_emoji("🇺🇸🇨🇦") will return both flags instead of the first emoji. Stop after the first pair and add a regression test for adjacent flags.

Suggested fix
-fn extract_first_emoji(text: &str) -> Option<String> {
-    let mut chars = text.chars();
+fn extract_first_emoji(text: &str) -> Option<String> {
+    let mut chars = text.chars().peekable();
     while let Some(ch) = chars.next() {
         // Regional indicator pair → flag emoji (e.g. 🇺🇸 = U+1F1FA U+1F1F8)
         if is_regional_indicator(ch) {
             let mut emoji = String::new();
             emoji.push(ch);
-            // Consume consecutive regional indicators (flags are pairs)
-            for next in chars.by_ref() {
-                if is_regional_indicator(next) {
-                    emoji.push(next);
-                } else {
-                    break;
-                }
+            if let Some(next) = chars.peek().copied() {
+                if is_regional_indicator(next) {
+                    emoji.push(next);
+                    chars.next();
+                    return Some(emoji);
+                }
             }
-            return Some(emoji);
+            continue;
         }
     fn extract_flag_emoji_keeps_pair_together() {
         assert_eq!(extract_first_emoji("🇺🇸"), Some("🇺🇸".to_string()));
         assert_eq!(
             extract_first_emoji("🇬🇧 Great Britain"),
             Some("🇬🇧".to_string())
         );
+        assert_eq!(
+            extract_first_emoji("🇺🇸🇨🇦"),
+            Some("🇺🇸".to_string())
+        );
     }

Also applies to: 687-693

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/openhuman/local_ai/ops.rs` around lines 589 - 604, extract_first_emoji
currently consumes all consecutive regional indicators, so adjacent flags like
"🇺🇸🇨🇦" are returned as one reaction; change the regional-indicator handling
in extract_first_emoji to collect at most two regional indicators (a single
pair) and then stop/return immediately (i.e., if the first char is a regional
indicator, optionally consume one more regional indicator to form the flag and
return that string), and add a regression test asserting
extract_first_emoji("🇺🇸🇨🇦") == Some("🇺🇸".to_string()); apply the same
one-or-two regional-indicator limit to the other similar regional-indicator
handling block in this file.

@senamakel senamakel merged commit 207ec7d into main Apr 1, 2026
13 checks passed
@senamakel senamakel deleted the feat/message-reactions branch April 4, 2026 21:55
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
…yhumansai#181)

* feat(conversations): implement auto-reaction feature for user messages

- Added functionality to automatically react to user messages with emojis based on the local AI's evaluation of the message content and channel type.
- Introduced `maybeAutoReact` function to handle the decision-making process for emoji reactions.
- Updated the `Conversations` component to store the last user message and trigger reactions accordingly.
- Enhanced the `tauriCommands` with a new method `openhumanLocalAiShouldReact` to facilitate local model evaluations for reactions.
- Updated local AI operations to include reaction decision logic, ensuring efficient and context-aware responses.

This feature enhances user engagement by adding a personal touch to interactions.

* style: fix prettier formatting in Conversations.tsx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: apply cargo fmt to ops.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review — race condition, invisible reactions, flag emoji, PII log

- Replace lastUserMessageRef with per-thread pendingReactionRef Map so
  cancellation/retry clears stale entries and onDone only applies to
  the matching request round
- Render reactions on user message bubbles (not just agent messages)
  so auto-reactions are actually visible; manual picker stays agent-only
- Fix extract_first_emoji to consume consecutive regional indicator
  symbols as a single flag emoji (e.g. 🇺🇸); add regression test
- Replace raw_output in should_react debug log with output_len to
  avoid persisting user PII in traces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: apply cargo fmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(conversations): remove unused imports for local AI transcription and TTS

- Cleaned up the Conversations component by removing unused imports related to local AI transcription and text-to-speech functionalities, streamlining the codebase and improving maintainability.es

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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