From 7e180be2c38e1347c0033208945a87b838fef3ab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 08:09:56 +0000 Subject: [PATCH 1/2] feat(android): bring back quick replies in the terminal behind a button The terminal reply box lost its quick-replies row to make room for the terminal. Offer the same saved replies again, behind a star button in the reply box's action row: it opens a menu of the replies and costs no extra row, so it also stays available with the terminal expanded. Picking one adds it to the draft, or sends it straight to the agent when "Send quick replies immediately" is on. The button is hidden when there are no quick replies. Settings and the README now say where they appear. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01H9KgFkWtugnRn7sAS5csQx --- android/README.md | 2 +- .../com/parallelcode/phone/ComposerExtras.kt | 45 +++++++++++++++++++ .../com/parallelcode/phone/MainActivity.kt | 2 + .../java/com/parallelcode/phone/Screens.kt | 15 +++++++ .../com/parallelcode/phone/SettingsScreen.kt | 4 +- 5 files changed, 65 insertions(+), 3 deletions(-) diff --git a/android/README.md b/android/README.md index cfb691426..8a5f4eeab 100644 --- a/android/README.md +++ b/android/README.md @@ -16,7 +16,7 @@ Native companion app for the desktop's **Connect Phone** (Remote Access) feature - **Terminal space and zoom:** pinch with two fingers to magnify the whole terminal up to 400%, and pan with two fingers (or sideways with one) to read a part of it; one finger still scrolls the history. Swiping between tasks pauses while zoomed; double-tap or the zoom pill resets it. The expand icon in the title bar hides the title, tabs, and quick keys while keeping the message field and Send/Stop available; the restore icon or Android Back returns to the normal layout without losing your draft. Terminal and chat replies use full-width multiline fields, with action buttons below. - **History:** opening a terminal loads the desktop's history (up to 10,000 lines), and the phone keeps up to 20,000 lines while the terminal stays open. - **Changes:** the task's diff against its base branch, file by file with added and removed lines. -- **Quick replies and voice:** saved replies above the built-in chat's reply box (edit them in Settings; the terminal leaves them out to make room) and a mic button that dictates with Android's speech recognizer. +- **Quick replies and voice:** saved replies above the built-in chat's reply box and behind the star button in the terminal's (edit them in Settings), and a mic button that dictates with Android's speech recognizer. - **Widget:** a home-screen widget with the agents that need you and the usage meters, updated while the app is connected. Settings → Widget sets its background transparency (opaque, 75%, 50% or 25%; the border fades with the card, so your wallpaper shows through) and its card color (Obsidian, Slate or Light, each with text colors that stay readable). - **Notes:** read a task's notes panel; edit and save it once paired. - **New task:** pick a project, agent and model, then describe the work; needs pairing. diff --git a/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt b/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt index ae89c0c25..0ae909a14 100644 --- a/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt +++ b/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt @@ -9,8 +9,10 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.Star import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -18,13 +20,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.AlertDialog import androidx.compose.material3.AssistChip +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext @@ -54,6 +61,44 @@ fun QuickReplies(replies: List, enabled: Boolean, onPick: (String) -> Un } } +/** + * The same saved replies behind one button, for a reply box with no row to spare: tapping the + * button lists them, and picking one hands it to [onPick]. Hidden when there are none. + */ +@Composable +fun QuickRepliesButton( + replies: List, + enabled: Boolean, + modifier: Modifier = Modifier, + onPick: (String) -> Unit, +) { + if (replies.isEmpty()) return + var open by remember { mutableStateOf(false) } + val haptic = LocalHapticFeedback.current + Box { + OutlinedButton( + onClick = { open = true }, + enabled = enabled, + modifier = modifier, + shape = MaterialTheme.shapes.large, + ) { + Icon(Icons.Filled.Star, contentDescription = "Quick replies") + } + DropdownMenu(expanded = open && enabled, onDismissRequest = { open = false }) { + replies.forEach { reply -> + DropdownMenuItem( + text = { Text(reply, maxLines = 2, overflow = TextOverflow.Ellipsis) }, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + open = false + onPick(reply) + }, + ) + } + } + } +} + /** Adds [text] to a draft, as typing it after what is already there would. */ fun appendToDraft(draft: String, text: String): String = when { draft.isBlank() -> text diff --git a/android/app/src/main/java/com/parallelcode/phone/MainActivity.kt b/android/app/src/main/java/com/parallelcode/phone/MainActivity.kt index feda628bd..a72bb588d 100644 --- a/android/app/src/main/java/com/parallelcode/phone/MainActivity.kt +++ b/android/app/src/main/java/com/parallelcode/phone/MainActivity.kt @@ -576,6 +576,8 @@ private fun PhoneApp(model: PhoneViewModel) { client = model.client, alwaysFollowOutput = alwaysFollowOutput, fitTerminalToPhone = fitTerminalToPhone && active, + quickReplies = quickReplies, + sendQuickReplies = sendQuickReplies, promptHistory = model.promptHistory, pageLabel = pageLabel, nextNeedingYou = nextTaskNeedingYou(agents, agentId), diff --git a/android/app/src/main/java/com/parallelcode/phone/Screens.kt b/android/app/src/main/java/com/parallelcode/phone/Screens.kt index 005636c44..a5a6c0c3a 100644 --- a/android/app/src/main/java/com/parallelcode/phone/Screens.kt +++ b/android/app/src/main/java/com/parallelcode/phone/Screens.kt @@ -953,6 +953,8 @@ fun AgentScreen( client: RemoteClient, alwaysFollowOutput: Boolean, fitTerminalToPhone: Boolean, + quickReplies: List = emptyList(), + sendQuickReplies: Boolean = false, promptHistory: PromptHistoryStore? = null, pageLabel: String? = null, nextNeedingYou: RemoteAgent? = null, @@ -1222,6 +1224,8 @@ fun AgentScreen( ReplyBox( agentId = agentId, compact = terminalExpanded, + quickReplies = quickReplies, + sendQuickReplies = sendQuickReplies, working = agent?.running == true && (agent.attention == "active" || agent.attention == "shell_busy"), promptHistory = promptHistory, @@ -1700,6 +1704,8 @@ private fun arrowKeyFor(label: String): Pair? = when (label private fun ReplyBox( agentId: String, compact: Boolean = false, + quickReplies: List = emptyList(), + sendQuickReplies: Boolean = false, working: Boolean = false, promptHistory: PromptHistoryStore? = null, needsInput: Boolean = false, @@ -1821,6 +1827,15 @@ private fun ReplyBox( enabled = !busy, modifier = Modifier.fillMaxHeight(), ) { setDraft(appendToDraft(draft, it)) } + // Behind a button rather than a row of chips, so the terminal keeps its room. + QuickRepliesButton( + quickReplies, + enabled = !busy, + modifier = Modifier.fillMaxHeight(), + ) { + // Sent straight away, a reply goes to the agent even while the draft is a shell command. + if (sendQuickReplies) run { send(it, false) } else setDraft(appendToDraft(draft, it)) + } if (promptHistory != null) { OutlinedButton( onClick = { showHistory = true }, diff --git a/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt b/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt index 797e37180..1a9041000 100644 --- a/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt +++ b/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt @@ -446,7 +446,7 @@ fun SettingsScreen( HorizontalDivider(thickness = 1.dp, color = AppTheme.extra.borderSubtle, modifier = Modifier.padding(vertical = 14.dp)) SettingSwitchRow( title = "Send quick replies immediately", - description = "Tapping a quick reply in a built-in chat sends it straight away. Off, it is added to your draft first.", + description = "Tapping a quick reply sends it straight to the agent. Off, it is added to your draft first.", checked = sendQuickReplies, onCheckedChange = onSendQuickRepliesChange, ) @@ -1076,7 +1076,7 @@ private fun QuickRepliesEditor(replies: List, onChange: (List) - color = MaterialTheme.colorScheme.onSurface, ) Text( - "Shown above the reply box in built-in chats; tap one to add it to your message. One per line.", + "Shown above the reply box in built-in chats and behind the star button in terminals; tap one to add it to your message. One per line.", style = MaterialTheme.typography.bodyMedium, color = AppTheme.extra.textMuted, ) From 3abbdbeb5a69e1073c97b51abf2114fc107e77c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 08:43:07 +0000 Subject: [PATCH 2/2] fix(android): address review findings for terminal quick replies - Picking a quick reply into an empty draft leaves shell mode, so Send gives it to the agent, as sending it straight away already does. - Ignore taps on the quick-replies menu while it animates closed, so a quick double tap cannot send a reply twice. - Correct the quick-replies copy in Settings, SettingsStore and README, and note that the button's modifier applies to the button itself. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01H9KgFkWtugnRn7sAS5csQx --- android/README.md | 2 +- .../main/java/com/parallelcode/phone/ComposerExtras.kt | 5 ++++- .../src/main/java/com/parallelcode/phone/Screens.kt | 10 ++++++++-- .../main/java/com/parallelcode/phone/SettingsScreen.kt | 2 +- .../main/java/com/parallelcode/phone/SettingsStore.kt | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/android/README.md b/android/README.md index 8a5f4eeab..d716a1638 100644 --- a/android/README.md +++ b/android/README.md @@ -16,7 +16,7 @@ Native companion app for the desktop's **Connect Phone** (Remote Access) feature - **Terminal space and zoom:** pinch with two fingers to magnify the whole terminal up to 400%, and pan with two fingers (or sideways with one) to read a part of it; one finger still scrolls the history. Swiping between tasks pauses while zoomed; double-tap or the zoom pill resets it. The expand icon in the title bar hides the title, tabs, and quick keys while keeping the message field and Send/Stop available; the restore icon or Android Back returns to the normal layout without losing your draft. Terminal and chat replies use full-width multiline fields, with action buttons below. - **History:** opening a terminal loads the desktop's history (up to 10,000 lines), and the phone keeps up to 20,000 lines while the terminal stays open. - **Changes:** the task's diff against its base branch, file by file with added and removed lines. -- **Quick replies and voice:** saved replies above the built-in chat's reply box and behind the star button in the terminal's (edit them in Settings), and a mic button that dictates with Android's speech recognizer. +- **Quick replies and voice:** saved replies above the built-in chat's reply box and behind the star button in the terminal's reply box (edit them in Settings), and a mic button that dictates with Android's speech recognizer. - **Widget:** a home-screen widget with the agents that need you and the usage meters, updated while the app is connected. Settings → Widget sets its background transparency (opaque, 75%, 50% or 25%; the border fades with the card, so your wallpaper shows through) and its card color (Obsidian, Slate or Light, each with text colors that stay readable). - **Notes:** read a task's notes panel; edit and save it once paired. - **New task:** pick a project, agent and model, then describe the work; needs pairing. diff --git a/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt b/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt index 0ae909a14..9c6e56a05 100644 --- a/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt +++ b/android/app/src/main/java/com/parallelcode/phone/ComposerExtras.kt @@ -63,7 +63,8 @@ fun QuickReplies(replies: List, enabled: Boolean, onPick: (String) -> Un /** * The same saved replies behind one button, for a reply box with no row to spare: tapping the - * button lists them, and picking one hands it to [onPick]. Hidden when there are none. + * button lists them, and picking one hands it to [onPick]. Hidden when there are none. [modifier] + * applies to the button itself. */ @Composable fun QuickRepliesButton( @@ -89,6 +90,8 @@ fun QuickRepliesButton( DropdownMenuItem( text = { Text(reply, maxLines = 2, overflow = TextOverflow.Ellipsis) }, onClick = { + // The closing menu still takes taps while it animates out: pick only once. + if (!open) return@DropdownMenuItem haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) open = false onPick(reply) diff --git a/android/app/src/main/java/com/parallelcode/phone/Screens.kt b/android/app/src/main/java/com/parallelcode/phone/Screens.kt index a5a6c0c3a..db6f27f45 100644 --- a/android/app/src/main/java/com/parallelcode/phone/Screens.kt +++ b/android/app/src/main/java/com/parallelcode/phone/Screens.kt @@ -1833,8 +1833,14 @@ private fun ReplyBox( enabled = !busy, modifier = Modifier.fillMaxHeight(), ) { - // Sent straight away, a reply goes to the agent even while the draft is a shell command. - if (sendQuickReplies) run { send(it, false) } else setDraft(appendToDraft(draft, it)) + // A quick reply is for the agent, sent straight away or not: one that starts the + // draft leaves shell mode, so Send cannot run it as a shell command. + if (sendQuickReplies) { + run { send(it, false) } + } else { + if (draft.isBlank()) shellMode = false + setDraft(appendToDraft(draft, it)) + } } if (promptHistory != null) { OutlinedButton( diff --git a/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt b/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt index 1a9041000..6c90454bc 100644 --- a/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt +++ b/android/app/src/main/java/com/parallelcode/phone/SettingsScreen.kt @@ -1076,7 +1076,7 @@ private fun QuickRepliesEditor(replies: List, onChange: (List) - color = MaterialTheme.colorScheme.onSurface, ) Text( - "Shown above the reply box in built-in chats and behind the star button in terminals; tap one to add it to your message. One per line.", + "Shown above the reply box in built-in chats and behind the star button in terminals. One per line.", style = MaterialTheme.typography.bodyMedium, color = AppTheme.extra.textMuted, ) diff --git a/android/app/src/main/java/com/parallelcode/phone/SettingsStore.kt b/android/app/src/main/java/com/parallelcode/phone/SettingsStore.kt index c86197fcc..a7c9c2430 100644 --- a/android/app/src/main/java/com/parallelcode/phone/SettingsStore.kt +++ b/android/app/src/main/java/com/parallelcode/phone/SettingsStore.kt @@ -64,7 +64,7 @@ class SettingsStore(private val prefs: SharedPreferences) { get() = prefs.getBoolean(KEY_FIT_TERMINAL, false) set(value) = prefs.edit { putBoolean(KEY_FIT_TERMINAL, value) } - /** Replies offered above the reply box, one per line. */ + /** Replies offered with the reply box (above it in chats, behind a button in terminals), one per line. */ var quickReplies: List get() = (prefs.getString(KEY_QUICK_REPLIES, null) ?: DEFAULT_QUICK_REPLIES.joinToString("\n")) .lines().map { it.trim() }.filter { it.isNotEmpty() }