Skip to content

feat(composer): add emoji picker and browser mic STT to composer toolbar#77

Open
adroidian wants to merge 3 commits into
agent0ai:mainfrom
adroidian:feat/composer-emoji-picker-and-mic-stt
Open

feat(composer): add emoji picker and browser mic STT to composer toolbar#77
adroidian wants to merge 3 commits into
agent0ai:mainfrom
adroidian:feat/composer-emoji-picker-and-mic-stt

Conversation

@adroidian

@adroidian adroidian commented Jun 14, 2026

Copy link
Copy Markdown

What this adds

Two new buttons in the onscreen agent composer toolbar, visible in full mode only:

😊 Emoji picker

  • Opens a popover with 90+ emoji across 7 categories (smileys, hearts, gestures, nature, food, objects, symbols)
  • Positioned via the existing positionPopover() utility with { align: 'end', placement: 'top' } — same pattern as the composer action menu
  • Inserts at cursor position with correct UTF-16 cursor advancement (emoji.length, not [...emoji].length) so stacking multiple emoji works correctly
  • Teleported to body via x-teleport to escape stacking contexts
  • Closes on outside click, Escape, or after insertion
  • Button shows active state while picker is open

🎤 Browser mic / STT

  • Uses the Web Speech API (webkitSpeechRecognition / SpeechRecognition)
  • Button is hidden automatically when the API is unavailable (Firefox, some privacy-hardened browsers)
  • Click to start → speak → auto-stops after a speech pause; click again to cancel early
  • Transcript is appended to the draft with smart spacing (adds a space if the draft doesn't already end with one)
  • Pulsing red animation while recording via CSS keyframes

Implementation notes

State — all lives in the Alpine store model object, no new top-level globals:

  • emojiPickerAnchor, emojiPickerPosition, isEmojiPickerVisible — picker positioning
  • isRecording, sttSupported, _sttRecognition — mic state

No external dependencies. CSS uses existing space design tokens (--space-danger, --space-accent, --space-surface-2, --space-border, --space-popover-z) with hard-coded fallbacks for themes that don't define them.

STT note: webkitSpeechRecognition streams audio to Google's cloud STT service and requires network access to speech.googleapis.com. Users behind strict firewalls or using privacy-focused browsers (Brave with default settings) may not be able to use the mic feature — the emoji picker is unaffected.

Bug fix bundled with this PR

While adding the mic button (which made the composer actions column taller), we noticed clicks in the dead zone below the textarea were being swallowed. Root cause: the composer uses display:grid with align-items:stretch, so .onscreen-agent-composer-input-wrap grows taller than the single-row textarea. Two-line fix:

  • panel.html: @click.self="$refs.input.focus()" on the input-wrap forwards dead-zone clicks to the textarea
  • onscreen-agent.css: cursor:text on the input-wrap so the dead zone looks interactive

Files changed

File Changes
store.js +198 lines — state properties, 3 getters (isEmojiPickerOpen, emojiPickerStyle, emojiList), 7 methods
panel.html +52 lines — emoji + mic buttons in composer actions, emoji picker teleport template
onscreen-agent.css +55 lines — mic pulse animation, emoji button active state, picker popover + grid styles

Testing

Tested in:

  • ✅ Chrome — emoji picker and mic STT both work
  • ✅ Firefox — emoji picker works, mic button correctly hidden (no Web Speech API)
  • ⚠️ Brave (default settings) — emoji works, mic fails with network error due to Brave blocking speech.googleapis.com

Add two new buttons to the onscreen agent composer toolbar (full mode only):

**Emoji picker**
- Smiley face button opens a popover with 90+ emoji across 7 categories
- Picker is positioned via the existing positionPopover() utility, matching
  the composerActionMenu pattern (anchor-aware, top/end alignment)
- Emoji inserts at cursor position with correct UTF-16 cursor advancement
- Teleported to body via x-teleport to escape stacking contexts
- Closes on outside click, Escape, or after insertion
- Button shows active state while picker is open

**Browser mic / STT**
- Mic button uses the Web Speech API (webkitSpeechRecognition / SpeechRecognition)
- Button hidden automatically when the API is not available (Firefox, etc.)
- Click to start, auto-stops after speech pause; click again to cancel
- Transcript appended to draft with smart spacing
- Pulsing red animation while recording

**Implementation notes**
- All state lives in the Alpine store (model object): emojiPickerAnchor,
  emojiPickerPosition, isEmojiPickerVisible, isRecording, sttSupported,
  _sttRecognition
- No external dependencies; CSS uses existing space design tokens with
  sensible hard-coded fallbacks
- Tested in Chrome (full functionality) and Firefox (emoji only, mic button
  correctly hidden); Brave blocks the STT network request by default
@adroidian

Copy link
Copy Markdown
Author

Heads up: this was written with AI assistance, but I personally tested it on our own deployment and verified the behavior described. Sharing in the spirit of transparency given how many repos are thinking about this.

The composer uses display:grid with align-items:stretch, so the
input-wrap grows taller than the textarea when action buttons are
tall. Clicks in the dead zone below the textarea were swallowed.

Adds @click.self on the input-wrap to forward those clicks to the
textarea, and cursor:text in CSS to match.
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