[ON HOLD — upstreamed as microsoft/conductor#510] PDA-35: Fix: discover interactive input treats each newline (including pasted multi-line text) as a separate prompt - #5
Conversation
…lines in human.py) and wired the terminal dialog gate to use it with a /send sentinel so pasted multi-line blocks and mid-paste EOF are handled as one turn instead of fragmenting/dismissing; added regression tests in both test files (lint/format clean; two pre-existing unrelated typecheck errors in claude_agent_sdk.py confirmed present before this change).
Independent review — head
|
The new tty branch in _get_user_input returned before the try/except that catches (EOFError, KeyboardInterrupt), so the dialog's main turn lost both of its non-sentinel exits: - Ctrl-D no longer dismissed. read_multiline_lines converts EOF into a returned string, so an empty read yielded "", which the `user_input == ""` guard sent back round the loop. _get_user_input could never return None on this path, leaving the dismissal branch unreachable and spinning on repeated EOF rather than exiting the grill. - Ctrl-C propagated out of handle_dialog uncaught, crashing the run. The docstring added alongside it claimed None was returned on KeyboardInterrupt; the code did not do that. Both were regressions against main, where every path ran through the handler and returned None. read_multiline_lines now returns (text, hit_eof) so a caller can tell a paste that ended at EOF from a deliberate Ctrl-D at an empty prompt. The dialog gate dismisses on (hit_eof and not text) and re-wraps the await in the existing except clause; a paste terminated by EOF still submits its content, so AC4 holds. HumanGateHandler._read_multiline unpacks and returns a plain str, so the human gate's contract is unchanged. The three existing dialog tests all ended with an explicit "done", which is why neither exit path was covered. The two new tests use a bare side_effect=EOFError()/KeyboardInterrupt() rather than a list, so a future regression hangs or raises instead of passing on an exhausted mock; with the fix reverted the Ctrl-D test hangs, and pytest is killed before printing a summary. Verified: tests/test_gates 72 passed (was 69); full suite 7624 passed, 3 failed, all three failing identically on clean origin/main (chmod 0o000 permission tests that do not hold on this filesystem). make lint clean; make typecheck reports the same 2 pre-existing diagnostics in claude_agent_sdk.py and none in the touched files. Addresses the two blocking findings from the review on PR #5. The /send discoverability gap (the Dialog Mode banner still advertises only done//done) is left unaddressed here.
Both blocking findings fixed in
|
Re-review — head
|
Correction to my re-review above — full suite did complete, and it passesIn my review I said the full suite "stalled at ~59% ... and was killed by my own That matches the number in the fix write-up exactly, so consider it re-verified rather than unverified. I misread a slow stretch of dots as a hang and should have waited for the process to exit before characterising it. The 3 failures are the known pre-existing ones, and I checked that directly rather than inheriting the claim — run against the clean Neither file references Everything else in the review stands unchanged: both original blocking findings resolved, no blocking findings, two non-blocking notes. The only remaining unverified item is the lack of a manual check in a real interactive terminal. 🤖 Generated with Claude Code |
The multi-line turn requires /send to submit, but the opening banner still told users only to "type your responses below" and to say done//done when finished -- so the first instruction a user reads omitted the one keystroke a turn now needs. The per-turn hint from read_multiline_lines did name it, but the banner is read first. The sentinel is passed to styled() as a positional argument alongside agent.name, so the banner cannot drift from DIALOG_SUBMIT_SENTINEL. The new test renders the panel for real and asserts against the constant rather than a literal, matching the approach in tests/test_cli/test_markup_injection.py, which renders this same banner to catch interpolation that a mocked Panel would hide. Verified: tests/test_gates and tests/test_cli/test_markup_injection.py 116 passed; make lint clean. Rendered at width 76 to confirm the wrapped text reads correctly. Addresses the non-blocking documentation finding carried across both reviews on PR #5.
Review of
|
| Mutation | Test outcome |
|---|---|
DIALOG_SUBMIT_SENTINEL = "/transmit" |
passes — banner rendered /transmit, tracking the constant |
banner reverted to the pre-84b9777 text |
fails — assertion not satisfied |
done on the tty multi-line path, with isatty patched True and input() driven from a list:
| Input | Result |
|---|---|
done, /send |
dismissed, provider not called |
done, EOF |
dismissed, provider not called |
/done, /send |
dismissed, provider not called |
hello, world, /send, done, /send |
one turn dispatched, then dismissed |
Non-tty path, driving handle_dialog with a non-tty stdin: banner shown (Dialog Mode present, mentions /send), Prompt.ask called twice — once for the engagement select, once for the turn with choices=None — and the read_multiline_lines hint absent, confirming the single-line branch.
Cumulative — git diff origin/main...HEAD --stat: 4 files, +282/-24, confined to gates/dialog.py, gates/human.py and their tests. Nothing unrelated picked up.
Not verified: make typecheck (2 pre-existing claude_agent_sdk.py diagnostics also on main) and the test_skills/test_plugins chmod permission tests, which are known pre-existing failures.
🤖 Generated with Claude Code
84b9777 added the /send hint to the Dialog Mode banner unconditionally, but _display_dialog_start is called for every terminal dialog (dialog.py:248) while the multi-line reader is gated on sys.stdin.isatty(). Off a tty the turn falls back to the single-line Prompt.ask branch, where the sentinel does nothing -- so the banner told the user to type something with no effect, and it was the only instruction shown there (read_multiline_lines' per-turn hint is correctly absent). This was a regression introduced by 84b9777: on main the banner named only done//done, which is accurate on both paths. The hint is now built behind the same sys.stdin.isatty() condition as the reader, with the [bold] markup kept in the styled() template -- styled() inserts *values* verbatim precisely so they cannot be parsed as markup, so interpolating pre-marked-up text would render a literal "[bold]". The test is now parametrised over isatty and asserts presence-iff-tty rather than mere presence, so neither direction can regress unnoticed; it also asserts no literal "[bold]" survives. Confirmed it fails when the gate is removed (the isatty=False case) and passes with it. Low impact -- the realistic non-tty consumer is the web dashboard, which returns via _web_handle_dialog (dialog.py:217) and never renders this banner -- but the gate costs three lines and the condition already existed. Verified: tests/test_gates and tests/test_cli/test_markup_injection.py 117 passed; make lint clean; rendered both paths at width 76. Separately confirmed this panel emits no bold ANSI even on main, so the flattening of nested [bold] inside [dim] is pre-existing and not introduced here. Addresses the non-blocking finding from the third review on PR #5.
Fourth review — head
|
| Mutation | Result |
|---|---|
| (a) hint made unconditional | ...[False] FAILS — ('/send' in rendered) is False |
| (b) hint removed entirely | ...[True] FAILS — ('/send' in rendered) is True |
Both fail in the correct direction, so neither over- nor under-advertising can regress unnoticed.
Rendering, both branches at widths 40/60/76/100/300. No literal [bold] at any width in either branch. Plain text and spans:
isatty=True "Agent 't' would like to discuss its output with you.\n
Type your response below. It can span multiple lines;
send it with /send on its own line. Say done or /done when finished."
spans: (0,9,bold) (53,175,dim) (120,125,bold)->/send (147,151,bold) (155,160,bold)
isatty=False "Agent 't' would like to discuss its output with you.\n
Type your response below. Say done or /done when finished."
spans: (0,9,bold) (53,111,dim) (83,87,bold) (91,96,bold)
No doubled or missing space at the join in either branch (" " in plain → False both times). The tty branch reads below. It can span…own line. Say done; the non-tty branch reads below. Say done.
The f-string alternative really would have broken. Value-interpolating the marked-up fragment gives:
'Type your response below. send it with [bold]/send[/bold] on its own line.' — literal brackets present, confirming the template-side markup is the correct choice.
Prior rounds undisturbed. 2b00340 touches only _display_dialog_start and its one test (16/3 and 13/4 lines). ef9f417's Ctrl-D/Ctrl-C work is present verbatim at HEAD (except (EOFError, KeyboardInterrupt) ×2, if hit_eof and not text: ×1, read_on_daemon_thread ×2 — same counts as at ef9f417). No test present at ef9f417 is missing at HEAD; the only test-name delta is the rename of the banner test to test_opening_banner_advertises_the_sentinel_only_on_a_tty.
Cumulative branch (git diff origin/main...HEAD) — 4 files, +304/-24: gates/dialog.py, gates/human.py and their two test files. Nothing outside the dialog/human gate surface.
sys is imported at dialog.py:14.
🤖 Generated with Claude Code
On hold — proposed upstream insteadThis repository is a fork of microsoft/conductor, and this defect is in upstream code rather than anything we added. Rather than carry a permanent fork-local patch, the fix has been raised upstream:
This PR stays open and unmerged until that is resolved.
The upstream version is not byte-identical to this branchRebased onto upstream
One claim in the ticket does not hold upstreamPDA-35's AC(4) says exhausting a paste ends the dialog. Verified against upstream So AC(4) is a valid requirement for our fork and not a description of the upstream defect. Issue microsoft#509 describes the measured behaviour instead. Also worth notingThe Verified against upstream: 🤖 Generated with Claude Code |
Important
On hold — do not merge yet. This fix has been proposed upstream, and the
outcome there decides what happens to this PR.
Accepted upstream → take it via an upstream sync, close this without merging.
Rejected or stalled → merge this and maintain it as a fork-local patch.
The upstream branch is not byte-identical to this one, and one AC in PDA-35
does not describe upstream behaviour — see
this comment.
Ticket: https://toogoodtogo.myjetbrains.com/issue/PDA-35
Automated delivery via the Pandora workflow.
Review notes
Review — PDA-35
Panel verdict: pass
Min score across lenses: 9
Guard decision: accept (after 1 round(s))
Verification (real exit codes, run by the pipeline)
Passed: True
Outstanding issues