Skip to content

Hold-to-talk voice input with live interim text in the composer, and a plugin API to build it #5623

Description

@benwaffle

Summary

I want to dictate the way most push-to-talk tools work. I hold a key, talk, and see the words show up as I speak. When I let go, the text lands in the composer. Today bb voice input is click to start, click to stop, and no text appears until the whole recording has been transcribed. I would like core to support this. Failing that, I would like plugins to get the two composer hooks needed to build it, because a local transcription plugin is the obvious place for it.

Versions and environment

  • bb 0.46.0, desktop app
  • macOS 26.6
  • Code checked against main at 05771e3c82404328854468f0fff7f0f8203e2076

Workflow I cannot complete today

  1. Open a thread and put the caret in an empty composer.
  2. I want to hold one key, speak, watch the transcript build up, and release to get editable text.
  3. What actually happens: I click the microphone button, speak with no text feedback, click stop, and wait for the full transcript.

Current behavior in code:

Requested behavior

  • Hold to talk. Hold Space for about 200 ms in an empty composer to start recording. Release to put the text into the draft. Escape cancels. Once the composer has text, Space types a space as usual. Space is also left alone in inputs, buttons, menus, and open dialogs. A quick tap still types a space.
  • Live interim text. While recording, show the transcript so far after the caret in a muted, italic style. This preview is not part of the draft until release, so undo, mentions, and submit never see it.
  • A setting to turn it off. For example "Hold Space to talk" in Settings > Voice Input.

How this relates to existing issues

Prototype

I have a working patch in my fork, benwaffle/bb, as the commit "Add push-to-talk with live interim transcription to the composer". It touches only the app and uses the existing transcription route:

  • A 200 ms hold threshold on Space, with the focus and empty-draft rules above. Escape cancels.
  • About once a second it re-transcribes the audio of the current segment through the existing voice-transcription route. It shows the result as a ProseMirror widget decoration after the caret, so the document is never changed.
  • Segments rotate every 20 s. A finished segment's final transcript becomes committed text, and the next segment continues on the same microphone stream.
  • A hold stops itself after 5 minutes.
  • A "Hold Space to talk" switch in Settings > Voice Input, stored client-side and on by default.

I can open a PR if you want this in core.

Plugin API gap

This would work best as a plugin, for example together with a local whisper transcription service. The transcription half is already possible, because a plugin can declare an AI service with transcribe (backend-contract.ts#L1956-L1978). The composer half is not. I checked the composer surfaces in the Plugin Guide (surfaces.ts#L449-L599) and the SDK contract:

Need Closest existing API Why it falls short
Show text that is not in the draft, after the caret richText.effects (app-contract.ts#L2611-L2624), setTextEffect (#L2892) Both only paint ranges of text that is already in the draft. Neither one can add a widget.
insert / replace (#L2801) These change the draft. Interim text rewritten every second would go into undo history and could be sent by mistake.
Own a press-and-hold key in the composer experimental_registerCommand with defaultShortcut (#L2024-L2035) run fires once on press. There is no release event and no way to tell a tap from a hold. A plain Space cannot be claimed only when the draft is empty.
actions (inline buttons) A button can do click-to-toggle, which is what core already has.

A minimal shape that would be enough, as an experimental_ addition to PluginComposerApi and ComposerCustomization:

interface PluginComposerApi {
  /** Show host-styled text after the caret that is not part of the draft. null clears it. Cleared on unmount or scope change. */
  experimental_setInterimText(text: string | null): void;
}

interface ComposerCustomization {
  /** Press-and-hold gesture on a key while the composer has focus. The host decides when the key is free (for example an empty draft with no open menu) and when a press counts as a hold. A tap keeps the key's normal behavior. */
  experimental_holdKey?: {
    key: string;
    when?: "empty-draft" | "always";
    onHoldStart(context: { composer: PluginComposerApi }): void;
    onHoldEnd(context: { composer: PluginComposerApi; cancelled: boolean }): void;
  };
}

Following the "core uses the plugin API" rule, core's own hold-to-talk would then use these same two members instead of a private decoration and key handler.

Ruled out

  • Not a duplicate of Add a keyboard shortcut for voice input #2955: that issue asks for a start/stop toggle shortcut, with no hold gesture and no interim text.
  • The existing composer APIs cannot do this (see the table above).
  • bb voice transcribe already covers the CLI side for one-shot transcription. Live interim text is a composer-only feature, so the SDK surface above is the parity point.

Suggested priority and effort

Low priority, medium effort. Click-to-dictate is a workaround. Hold-to-talk with live feedback is mostly a quality-of-life gain for people who dictate a lot.

AGENT GENERATED

Activity

  1. added
    uiApp shell, sidebar, composer, rendering
    pluginsPlugin SDK, runtime, marketplace
    on Oct 16, 2026
  2. bb-slop-cop commented on Oct 16, 2026

    @bb-slop-cop
    Contributor

    🚨 SLOP COP 🚨 · new-issue-autopilot

    Applied missing classifications: Type: Feature · Priority: Low · Effort: High. Added labels: ui, plugins. No existing classifications were replaced; final Type was re-read and verified.

    This adds a new composer interaction and public plugin API rather than correcting unexpected behavior. Existing click-to-dictate provides a workaround, so priority is Low; designing the gesture, interim-text lifecycle, and shared SDK surface makes effort High.

    No reproduction report or pull request: this is a feature request, so the bug-only report and fix steps were skipped.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    pluginsPlugin SDK, runtime, marketplaceuiApp shell, sidebar, composer, rendering

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions