feat: dictate replies from mobile Search preview (Closes #953)
All checks were successful
CI / lint (pull_request) Successful in 1m55s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 51s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-16 08:55:49 +00:00
parent 51710972eb
commit d63166b581
4 changed files with 50 additions and 0 deletions

View File

@ -644,6 +644,8 @@ textarea { resize: vertical; min-height: 120px; }
.search-preview-reply textarea { box-sizing:border-box; width:100%; min-height:110px; resize:vertical; scroll-margin-bottom:calc(190px + env(safe-area-inset-bottom)); }
.search-preview-reply-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.search-preview-reply-actions button { min-height:44px; }
.search-preview-reply .voice-conversation-controls button,
.search-preview-reply .voice-conversation-review-actions button { min-height:44px; }
.search-preview-navigation { display:grid; grid-template-columns:minmax(0,1fr) auto minmax(0,1fr); align-items:center; gap:8px; }
.search-preview-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
.search-preview-actions button, .search-preview-actions a { min-height:44px; box-sizing:border-box; display:flex; align-items:center; justify-content:center; }

View File

@ -640,9 +640,13 @@
const issueVoiceReply = mountConversationVoice('issue-comment', '#issue-comment');
const pullVoiceReply = mountConversationVoice('pull-comment', '#pull-comment');
const updateVoiceReply = mountConversationVoice('update-reply', '#update-reply');
const searchVoiceReply = mountConversationVoice('search-reply', '#search-preview-reply');
function conversationVoiceTarget(kind, item) {
return kind + ':' + String(item?.repository || '').trim().toLowerCase() + '#' + Number(item?.number);
}
function searchConversationVoiceTarget(item) {
return 'search:' + conversationVoiceTarget(item?.kind, item);
}
const createIssueAttachmentController = issueAttachment.mount({
input: qs('#create-issue-attachment'),
inputs: [qs('#take-create-issue-photo'), qs('#create-issue-attachment')],
@ -4945,6 +4949,7 @@
qs('#close-search-preview').textContent = searchPreviewReturnKind === 'today-readiness'
? 'Back to blockers' : 'Back to search';
if (state.status === 'closed') {
searchVoiceReply.cancel();
sheet.classList.remove('open');
return;
}
@ -4960,6 +4965,7 @@
renderSearchPreviewReply(state, null, searchPreview, document);
if (state.status === 'loading') {
searchVoiceReply.open(searchConversationVoiceTarget(state.item));
searchPreviewDetail = null;
qs('#search-preview-key').textContent = state.item.repository + ' #' + state.item.number;
qs('#search-preview-title').textContent = state.item.title || 'Work preview';

View File

@ -546,6 +546,22 @@
<section class="search-preview-reply" aria-labelledby="search-preview-reply-title" hidden>
<h2 id="search-preview-reply-title">Reply</h2>
<textarea id="search-preview-reply" maxlength="10000" placeholder="Write a reply"></textarea>
<section class="voice-conversation" id="voice-search-reply" aria-label="Dictate search result reply" hidden>
<div class="voice-conversation-controls">
<button id="start-voice-search-reply" type="button">Dictate reply</button>
<button id="stop-voice-search-reply" type="button" hidden>Stop listening</button>
</div>
<div id="voice-search-reply-status" class="small" aria-live="polite"></div>
<div class="voice-conversation-review" id="voice-search-reply-review" hidden>
<label for="voice-search-reply-transcript">Review transcript</label>
<textarea id="voice-search-reply-transcript" maxlength="10000"></textarea>
<div class="voice-conversation-review-actions">
<button id="append-voice-search-reply" type="button">Use transcript</button>
<button id="replace-with-voice-search-reply" type="button" hidden>Replace draft</button>
<button id="discard-voice-search-reply" type="button">Discard</button>
</div>
</div>
</section>
<div id="search-preview-reply-status" class="small" aria-live="polite"></div>
<div class="search-preview-reply-actions">
<button id="send-search-preview-reply" type="button">Send</button>

View File

@ -185,3 +185,29 @@ def test_all_mobile_conversation_composers_ship_accessible_voice_review_controls
assert "issueVoiceReply.cancel();" in dashboard
assert "pullVoiceReply.cancel();" in dashboard
assert "updateVoiceReply.cancel();" in dashboard
def test_mobile_search_reply_ships_target_scoped_voice_review_and_cancels_on_close():
html = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
for control in (
"voice-search-reply",
"start-voice-search-reply",
"stop-voice-search-reply",
"voice-search-reply-review",
"voice-search-reply-transcript",
"append-voice-search-reply",
"replace-with-voice-search-reply",
"discard-voice-search-reply",
"voice-search-reply-status",
):
assert f'id="{control}"' in html
assert 'for="voice-search-reply-transcript"' in html
assert "mountConversationVoice('search-reply', '#search-preview-reply')" in dashboard
assert "searchVoiceReply.open(searchConversationVoiceTarget(state.item))" in dashboard
assert "searchVoiceReply.cancel();" in dashboard
assert ".search-preview-reply .voice-conversation-controls button" in css
assert "min-height:44px" in css