Skip to content

Support piped stdout in dictate: auto-start single utterance#193

Merged
alexkroman merged 2 commits into
mainfrom
claude/vigilant-faraday-13jyln
Jun 16, 2026
Merged

Support piped stdout in dictate: auto-start single utterance#193
alexkroman merged 2 commits into
mainfrom
claude/vigilant-faraday-13jyln

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

Enable assembly dictate to work in pipelines by detecting when stdout is not a TTY and automatically recording a single utterance without requiring a toggle keystroke.

Summary

When assembly dictate is piped to another command (e.g., assembly dictate | assembly llm "…"), the downstream consumer blocks waiting for input while dictate idles in its interactive loop. This change detects piped stdout and switches to single-shot mode, which auto-starts recording and exits after one utterance so the transcript flows to the next stage.

Key Changes

  • Extract _capture_and_transcribe() helper: Consolidates the record-and-transcribe logic previously duplicated in the session loop, reducing code duplication and enabling reuse for both interactive and single-shot modes.

  • Add single parameter to _session(): Controls whether to auto-start one utterance (piped or --once) or enter the interactive idle-toggle loop. The docstring clarifies the two modes and their use cases.

  • Detect piped stdout in run_dictate(): Import stdio module and call stdio.stdout_is_tty() to determine if stdout is a pipe. Set single = opts.once or not stdio.stdout_is_tty() to enable single-shot mode for both --once flag and piped scenarios.

  • Conditional start prompt: Only show the interactive "Press Enter to start recording…" prompt when in interactive mode (not single), since single-shot mode announces "● Recording" when the mic opens.

  • Update help text and examples:

    • Clarify --once help: "Record one utterance immediately, then exit"
    • Expand command docstring to document piped and --once behavior
    • Add example: assembly dictate | assembly llm "write a conventional commit"
  • Test coverage: Add test_piped_stdout_auto_starts_one_utterance_then_exits() to verify that piped stdout triggers single-shot mode, auto-starts recording, and exits after one utterance. Mock stdio.stdout_is_tty() to return False and verify the session reads no blocking idle key (only the zero-timeout in-recording poll).

Implementation Details

  • The stdio module is imported alongside sync_stt in _exec.py to check TTY status.
  • Test seams mock stdio.stdout_is_tty() to default to True (interactive), preventing capsys from forcing single-utterance mode in unrelated tests.
  • The single-shot path calls _capture_and_transcribe() once and returns, while the interactive path loops until a quit key or --once flag.

https://claude.ai/code/session_01KchiKPHFyhKBpQf6QkeyfT

`assembly dictate | assembly llm "…"` hung: dictate loops until `q`, so a
piped stdout never closed and the downstream command blocked on stdin
forever — pressing Enter stopped the recording and transcribed it, but the
text never reached the next stage.

When stdout is not a tty (or `--once` is set), run a single-shot session:
recording auto-starts so one capture takes a single keystroke to stop, then
dictate exits, closing the pipe and unblocking the consumer. The interactive
loop (toggle to start, q to quit) is unchanged when stdout is a terminal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KchiKPHFyhKBpQf6QkeyfT
@alexkroman alexkroman enabled auto-merge June 16, 2026 21:21
# dictate exits, so a looping session would keep the downstream consumer
# blocked on stdin forever. Single-shot mode (piped or --once) records
# one utterance and exits so the transcript drains to the next stage.
single = opts.once or not stdio.stdout_is_tty()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

single and _session(...) are inside if opts.prompt and opts.language, so dictation only runs when both flags are set; otherwise run_dictate() exits without recording or transcribing.

Details

✨ AI Reasoning
​The updated flow is trying to always run dictation while optionally warning when --prompt and --language are combined. However, the new session-start logic is placed under the same condition as the warning. That means the command loop runs only when both options are provided together. In the common case where that condition is false, execution exits the context manager without ever entering recording/transcription, so the command effectively does nothing. This is a definite logic bug caused by control-flow placement, not a style issue.

🔧 How do I fix it?
Trace execution paths carefully. Ensure precondition checks happen before using values, validate ranges before checking impossible conditions, and don't check for states that the code has already ruled out.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@AikidoSec ignore: False positive — misread indentation. The if opts.prompt and opts.language: block contains only the emit_warning(...) call; single = … and _session(...) are dedented to the with TerminalKeys() scope and run unconditionally. Verified by test_hotkey_records_then_prints_bare_transcript, which records and transcribes with neither flag set, plus 100% patch coverage on this file.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Based on your feedback, we ignored this issue because of the following reason:

False positive — misread indentation. The if opts.prompt and opts.language: block contains only the emit_warning(...) call; single = … and _session(...) are dedented to the with TerminalKeys() scope and run unconditionally. Verified by test_hotkey_records_then_prints_bare_transcript, which records and transcribes with neither flag set, plus 100% patch coverage on this file.


Generated by Claude Code

…ay-13jyln

# Conflicts:
#	aai_cli/commands/dictate/_exec.py
@alexkroman alexkroman added this pull request to the merge queue Jun 16, 2026
Merged via the queue into main with commit 95effaf Jun 16, 2026
19 checks passed
@alexkroman alexkroman deleted the claude/vigilant-faraday-13jyln branch June 16, 2026 22:45
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.

2 participants