Support piped stdout in dictate: auto-start single utterance#193
Conversation
`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
| # 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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
@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
There was a problem hiding this comment.
✅ 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 theemit_warning(...)call;single = …and_session(...)are dedented to thewith TerminalKeys()scope and run unconditionally. Verified bytest_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
Enable
assembly dictateto 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 dictateis 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
singleparameter 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(): Importstdiomodule and callstdio.stdout_is_tty()to determine if stdout is a pipe. Setsingle = opts.once or not stdio.stdout_is_tty()to enable single-shot mode for both--onceflag 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:
--oncehelp: "Record one utterance immediately, then exit"--oncebehaviorassembly 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. Mockstdio.stdout_is_tty()to returnFalseand verify the session reads no blocking idle key (only the zero-timeout in-recording poll).Implementation Details
stdiomodule is imported alongsidesync_sttin_exec.pyto check TTY status.stdio.stdout_is_tty()to default toTrue(interactive), preventing capsys from forcing single-utterance mode in unrelated tests._capture_and_transcribe()once and returns, while the interactive path loops until a quit key or--onceflag.https://claude.ai/code/session_01KchiKPHFyhKBpQf6QkeyfT