chore: refresh chat UI cleanup against dev

This commit is contained in:
Bailey Dixon
2026-09-13 20:13:40 -04:00
21 changed files with 255 additions and 76 deletions
+1
View File
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- Android shows standalone response cards without an outer bubble, uses subtler assistant surfaces, and places delivery status beside message timestamps.
- Android answers upstream Clarify batches one question at a time, with independent choices, custom answers, and confirmed progress preserved across reconnects. (#474)
- Android context previews mark phone status and turn context as unavailable in Gateway chats instead of claiming they are sent. Settings clarify that automatic phone-status sharing applies to API-only chats. (#556)
- Relay Dashboard WebSockets work with current Hermes authentication helpers while preserving older-host compatibility, single-use tickets, Host/Origin/IP checks, and Relay session authentication.
- Android shows Hermes profile display names and groups the resolved server default under its agent identity, while preserving explicit profile selection and saved conversations.
@@ -23,14 +23,9 @@ import com.hermesandroid.relay.R
import com.hermesandroid.relay.viewmodel.ChatViewModel
/**
* Bottom-sheet audit of the exact extra context the agent is injected with on
* the next turn — opened by tapping the chat [ContextMeterBar].
*
* Renders the SAME [ChatViewModel.InjectedContext] the send path builds (via
* [ChatViewModel.previewInjectedContext] → `composeInjectedContext`), so it is
* a faithful audit, not a re-derivation that could drift. Empty blocks show a
* labeled note instead of vanishing, and the gateway's server-side persona is
* explicitly called out as not-sent-from-this-device.
* Transport-aware context preview, opened from the chat [ContextMeterBar].
* Unsupported blocks are labeled rather than represented as delivered.
* Server-reported configuration is separate from device-supplied context.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -53,7 +48,7 @@ fun InjectedContextSheet(
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.injected_context_subtitle, context.transport),
text = stringResource(R.string.injected_context_subtitle),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -71,6 +66,7 @@ fun InjectedContextSheet(
val mediaNoRelay = stringResource(R.string.injected_context_media_no_relay)
val relayNotSet = stringResource(R.string.injected_context_relay_not_set)
val turnNotSet = stringResource(R.string.injected_context_turn_not_set)
val unsupported = stringResource(R.string.injected_context_gateway_unsupported)
ContextSection(
title = personaTitle,
@@ -84,7 +80,7 @@ fun InjectedContextSheet(
ContextSection(
title = phoneStatusTitle,
body = context.appContext,
emptyNote = phoneStatusNotSet,
emptyNote = if (context.perTurnContextSupported) phoneStatusNotSet else unsupported,
)
ContextSection(
title = mediaTitle,
@@ -107,7 +103,7 @@ fun InjectedContextSheet(
ContextSection(
title = turnTitle,
body = context.interfaceContext,
emptyNote = turnNotSet,
emptyNote = if (context.perTurnContextSupported) turnNotSet else unsupported,
)
}
}
@@ -3215,8 +3215,8 @@ fun ChatScreen(
)
}
if (!supervised && showContextSheet) {
// Live audit of the exact extra context the agent will be
// injected with on the next turn (transparency / auditability).
// Snapshot of supported client context and separately reported
// server configuration, not a delivery receipt.
InjectedContextSheet(
context = remember(showContextSheet) {
chatViewModel.previewInjectedContext()
@@ -9770,8 +9770,8 @@ class ChatViewModel : ViewModel() {
* 0. **Gateway transport** — the server owns the persona end-to-end: the
* session is bound to the selected profile (SOUL applied server-side) and
* the personality overlay rides `config.set`/`ephemeral_system_prompt`.
* The phone sends NO persona/profile prompt (only the phone-status block)
* so it can't double-apply. Cases 1–3 are the SSE-fallback rules.
* The phone sends no per-turn system context on Gateway.
* Cases 1–3 are the explicit API-only transport rules.
* 1. **Selected profile with a non-blank [Profile.systemMessage]** —
* profile wins outright. Profile is a richer, newer concept than
* personality: it bundles model + persona (from the profile's
@@ -9786,15 +9786,12 @@ class ChatViewModel : ViewModel() {
* configured default.
*
* The phone-status [appContextSettings] block is appended to whichever
* of the above wins (or sent alone in case 3), so the LLM always sees
* phone state regardless of persona source.
* of the API-only cases above wins (or sent alone in case 3).
*/
/**
* The exact `system_message` (`ephemeral_system_prompt`) injected for a
* turn, split into labeled blocks. Single source of truth shared by
* [startStream] (which sends [combinedSystemMessage]) and
* [previewInjectedContext] (which renders it in the chat audit sheet) so
* the preview can never drift from what is actually sent.
* Context prepared for the selected transport, split into labeled blocks.
* This is a preview, not a delivery receipt or the complete agent prompt.
* Gateway has no general per-turn system-context slot.
*/
data class InjectedContext(
val personaPrompt: String?,
@@ -9825,6 +9822,8 @@ class ChatViewModel : ViewModel() {
* audit UI labels that block "added server-side".
*/
val personaOwnedServerSide: Boolean,
/** False on Gateway; unsupported blocks are excluded from the payload and preview. */
val perTurnContextSupported: Boolean,
)
/**
@@ -9860,11 +9859,14 @@ class ChatViewModel : ViewModel() {
} else {
_sseToolNames.value
}
val appContextRaw = buildPromptBlock(
settings = appContextSettings,
snapshot = capturePhoneSnapshot(),
availableTools = availableTools,
)
val appContextRaw = if (!gateway && appContextSettings.master) {
buildPromptBlock(
settings = appContextSettings,
snapshot = capturePhoneSnapshot(),
availableTools = availableTools,
)
} else null
val interfaceContext = interfaceContextPrompt?.takeIf { !gateway && it.isNotBlank() }
// Gateway has no per-turn system slot. SSE carries the standard
// Dashboard route first, then the optional Relay enhancement.
val upstreamMediaAvailable = dashboardMediaClientProvider?.invoke() != null
@@ -9876,25 +9878,26 @@ class ChatViewModel : ViewModel() {
// block (a stable environment fact, like phone status) and before the
// per-turn interface context. The per-block fields below null out blanks
// only for display.
val combined = listOfNotNull(personaPrompt, appContextRaw, mediaCapability, interfaceContextPrompt)
val combined = listOfNotNull(personaPrompt, appContextRaw, mediaCapability, interfaceContext)
.joinToString("\n\n")
.ifBlank { null }
return InjectedContext(
personaPrompt = personaPrompt,
appContext = appContextRaw?.takeIf { it.isNotBlank() },
interfaceContext = interfaceContextPrompt?.takeIf { it.isNotBlank() },
interfaceContext = interfaceContext,
mediaCapability = mediaCapability,
relayServerBlocks = relayServerBlocks,
relayMediaAvailable = relayMediaAvailable,
combinedSystemMessage = combined,
transport = streamingEndpoint,
personaOwnedServerSide = gateway,
perTurnContextSupported = !gateway,
)
}
/**
* Live audit snapshot of what the agent will be injected with on the next
* turn — rendered by the chat context sheet. Per-turn voice context is null
* Preview of context supported by the selected transport, plus separately
* reported Relay configuration. Per-turn voice context is null
* here (it's set only on a spoken turn); the UI notes that.
*/
fun previewInjectedContext(): InjectedContext {
+5 -4
View File
@@ -762,7 +762,7 @@
<string name="chat_settings_notify_when_finishes">Alertas de chat em segundo plano</string>
<string name="chat_settings_notify_when_finishes_desc">Notifique quando o Hermes precisar de uma resposta ou terminar em segundo plano</string>
<string name="chat_settings_share_phone_status">Compartilhar o status do celular com o agente</string>
<string name="chat_settings_share_phone_status_desc">Inclua uma breve mensagem do sistema sobre o app e o celular em cada turno do chat</string>
<string name="chat_settings_share_phone_status_desc">Incluir contexto do app e do telefone em chats apenas por API. Não é enviado nos chats padrão do Gateway.</string>
<string name="chat_settings_bridge_permissions">Bridge + permissões</string>
<string name="chat_settings_bridge_permissions_desc">Se acessibilidade, captura de tela, sobreposição e notificações foram concedidas</string>
<string name="chat_settings_foreground_app">App em primeiro plano</string>
@@ -771,7 +771,7 @@
<string name="chat_settings_battery_level_desc">Porcentagem atual da bateria. Desativado por padrão.</string>
<string name="chat_settings_safety_rails">Proteções de segurança</string>
<string name="chat_settings_safety_rails_desc">Quantidade de itens bloqueados, quantidade de verbos destrutivos e temporizador de desativação automática</string>
<string name="chat_settings_preview">Prévia</string>
<string name="chat_settings_preview">Exemplo de chat apenas por API</string>
<string name="chat_settings_no_system_message">(nenhuma mensagem do sistema será enviada)</string>
<string name="chat_settings_parse_tool_annotations">Analisar anotações de ferramentas</string>
<string name="chat_settings_experimental">Experimental</string>
@@ -3201,10 +3201,10 @@
<string name="injected_context_phone_status_title">Status do celular</string>
<string name="injected_context_relay_not_set">Nenhum relay configurado</string>
<string name="injected_context_relay_title">Relay</string>
<string name="injected_context_subtitle">Contexto enviado com a próxima mensagem</string>
<string name="injected_context_subtitle">Prévia deste chat. O contexto do servidor é configurado separadamente.</string>
<string name="injected_context_title">Contexto injetado</string>
<string name="injected_context_turn_not_set">Não definido</string>
<string name="injected_context_turn_title">Modo de detecção de turno</string>
<string name="injected_context_turn_title">Contexto do turno</string>
<string name="model_picker_cd_clear">Limpar</string>
<string name="model_picker_count">%d modelos</string>
<string name="power_feature_back_desc">Voltar</string>
@@ -4452,4 +4452,5 @@
<string name="voice_overlay_setup_start">Iniciar sobreposição de voz</string>
<string name="voice_overlay_settings_hint">Opcional nas duas versões. Abra o foco de voz no Chat e escolha Sobreposição. As permissões sozinhas nunca iniciam a escuta.</string>
<string name="voice_overlay_reset_position">Redefinir posição</string>
<string name="injected_context_gateway_unsupported">Não é enviado no chat do Gateway. Esta conexão não oferece suporte a contexto adicional por turno.</string>
</resources>
@@ -801,7 +801,7 @@
<string name="chat_settings_notify_when_finishes">后台聊天提醒</string>
<string name="chat_settings_notify_when_finishes_desc">Hermes 在后台需要输入或完成回复时通知</string>
<string name="chat_settings_share_phone_status">与代理分享手机状态</string>
<string name="chat_settings_share_phone_status_desc">每轮聊天附带一条关于应用和手机的简短系统消息</string>
<string name="chat_settings_share_phone_status_desc">在仅使用 API 的聊天中包含应用和手机上下文。标准 Gateway 聊天不会发送此内容。</string>
<string name="chat_settings_bridge_permissions">Bridge + 权限</string>
<string name="chat_settings_bridge_permissions_desc">无障碍、屏幕捕获、悬浮窗、通知是否已授权</string>
<string name="chat_settings_foreground_app">前台应用</string>
@@ -810,7 +810,7 @@
<string name="chat_settings_battery_level_desc">当前电池百分比。默认关闭。</string>
<string name="chat_settings_safety_rails">安全护栏</string>
<string name="chat_settings_safety_rails_desc">黑名单数量、破坏性动词数量和自动禁用计时器</string>
<string name="chat_settings_preview">预览</string>
<string name="chat_settings_preview">仅使用 API 的聊天示例</string>
<string name="chat_settings_no_system_message">(不会发送系统消息)</string>
<string name="chat_settings_parse_tool_annotations">解析工具标注</string>
<string name="chat_settings_experimental">实验性</string>
@@ -3300,10 +3300,10 @@
<string name="injected_context_phone_status_title">手机状态</string>
<string name="injected_context_relay_not_set">未配置 Relay</string>
<string name="injected_context_relay_title">Relay</string>
<string name="injected_context_subtitle">随下一条消息发送的上下文</string>
<string name="injected_context_subtitle">此聊天的预览。服务器端上下文单独配置。</string>
<string name="injected_context_title">注入上下文</string>
<string name="injected_context_turn_not_set">未设置</string>
<string name="injected_context_turn_title">轮换检测模式</string>
<string name="injected_context_turn_title">本轮上下文</string>
<string name="model_picker_cd_clear">清空</string>
<string name="model_picker_count">%d 个模型</string>
<string name="power_feature_back_desc">返回</string>
@@ -4533,4 +4533,5 @@
<string name="voice_overlay_setup_start">启动语音悬浮窗</string>
<string name="voice_overlay_settings_hint">两个版本均可选择使用。在聊天的语音专注模式中选择悬浮窗。仅授予权限不会开始监听。</string>
<string name="voice_overlay_reset_position">重置位置</string>
<string name="injected_context_gateway_unsupported">Gateway 聊天不会发送此内容。此连接不支持每轮附加上下文。</string>
</resources>
+5 -4
View File
@@ -804,7 +804,7 @@
<string name="chat_settings_notify_when_finishes">Chat-Benachrichtigungen im Hintergrund</string>
<string name="chat_settings_notify_when_finishes_desc">Benachrichtigen, wenn Hermes Eingaben benötigt oder im Hintergrund fertig wird</string>
<string name="chat_settings_share_phone_status">Smartphone-Status mit Agent teilen</string>
<string name="chat_settings_share_phone_status_desc">Bei jedem Chatdurchlauf eine kurze Systemnachricht über App und Smartphone einfügen</string>
<string name="chat_settings_share_phone_status_desc">App- und Smartphone-Kontext in reinen API-Chats mitsenden. Wird in Standard-Gateway-Chats nicht gesendet.</string>
<string name="chat_settings_bridge_permissions">Bridge + Berechtigungen</string>
<string name="chat_settings_bridge_permissions_desc">Ob Bedienungshilfe, Bildschirmaufnahme, Overlay und Benachrichtigungen erlaubt sind</string>
<string name="chat_settings_foreground_app">App im Vordergrund</string>
@@ -813,7 +813,7 @@
<string name="chat_settings_battery_level_desc">Aktueller Akkustand in Prozent. Standardmäßig aus.</string>
<string name="chat_settings_safety_rails">Schutzmechanismen</string>
<string name="chat_settings_safety_rails_desc">Anzahl gesperrter Einträge und destruktiver Verben sowie Timer zur automatischen Deaktivierung</string>
<string name="chat_settings_preview">Vorschau</string>
<string name="chat_settings_preview">Beispiel für einen reinen API-Chat</string>
<string name="chat_settings_no_system_message">(es wird keine Systemnachricht gesendet)</string>
<string name="chat_settings_parse_tool_annotations">Werkzeughinweise auswerten</string>
<string name="chat_settings_experimental">Experimentell</string>
@@ -3369,10 +3369,10 @@
<string name="injected_context_phone_status_title">Smartphone-Status</string>
<string name="injected_context_relay_not_set">Kein Relay konfiguriert</string>
<string name="injected_context_relay_title">Relay</string>
<string name="injected_context_subtitle">Kontext, der mit der nächsten Nachricht gesendet wird</string>
<string name="injected_context_subtitle">Vorschau für diesen Chat. Serverseitiger Kontext wird separat konfiguriert.</string>
<string name="injected_context_title">Eingefügter Kontext</string>
<string name="injected_context_turn_not_set">Nicht festgelegt</string>
<string name="injected_context_turn_title">Modus zur Durchlauferkennung</string>
<string name="injected_context_turn_title">Nachrichtenkontext</string>
<string name="model_picker_cd_clear">Leeren</string>
<string name="model_picker_count">%d Modelle</string>
<string name="power_feature_back_desc">Zurück</string>
@@ -4609,4 +4609,5 @@
<string name="voice_overlay_setup_start">Sprach-Overlay starten</string>
<string name="voice_overlay_settings_hint">In beiden Versionen optional. Öffne den Sprachfokus im Chat und wähle Overlay. Berechtigungen allein starten kein Zuhören.</string>
<string name="voice_overlay_reset_position">Position zurücksetzen</string>
<string name="injected_context_gateway_unsupported">Wird im Gateway-Chat nicht gesendet. Diese Verbindung unterstützt keinen zusätzlichen Kontext pro Nachricht.</string>
</resources>
+5 -4
View File
@@ -729,7 +729,7 @@
<string name="chat_settings_notify_when_finishes">Alertas de chat en segundo plano</string>
<string name="chat_settings_notify_when_finishes_desc">Notificar cuando Hermes necesite información o termine en segundo plano</string>
<string name="chat_settings_share_phone_status">Compartir el estado del teléfono con el agente</string>
<string name="chat_settings_share_phone_status_desc">Incluya un breve mensaje del sistema sobre la aplicación y el teléfono en cada turno de chat.</string>
<string name="chat_settings_share_phone_status_desc">Incluir contexto de la app y el teléfono en chats solo por API. No se envía en los chats estándar de Gateway.</string>
<string name="chat_settings_bridge_permissions">Bridge + permisos</string>
<string name="chat_settings_bridge_permissions_desc">Si se concede accesibilidad, captura de pantalla, superposición o notificaciones</string>
<string name="chat_settings_foreground_app">Aplicación en primer plano</string>
@@ -738,7 +738,7 @@
<string name="chat_settings_battery_level_desc">Porcentaje de batería actual. Desactivado de forma predeterminada.</string>
<string name="chat_settings_safety_rails">Barandillas de seguridad</string>
<string name="chat_settings_safety_rails_desc">Recuento de listas de bloqueo, recuento de verbos destructivos y temporizador de desactivación automática</string>
<string name="chat_settings_preview">Avance</string>
<string name="chat_settings_preview">Ejemplo de chat solo por API</string>
<string name="chat_settings_no_system_message">(no se enviará ningún mensaje del sistema)</string>
<string name="chat_settings_parse_tool_annotations">Anotaciones de la herramienta de análisis</string>
<string name="chat_settings_experimental">Experimental</string>
@@ -3033,10 +3033,10 @@
<string name="injected_context_phone_status_title">Estado del teléfono</string>
<string name="injected_context_relay_not_set">Ningún relay configurado</string>
<string name="injected_context_relay_title">Relay</string>
<string name="injected_context_subtitle">Contexto enviado con el siguiente mensaje.</string>
<string name="injected_context_subtitle">Vista previa de este chat. El contexto del servidor se configura por separado.</string>
<string name="injected_context_title">Contexto inyectado</string>
<string name="injected_context_turn_not_set">No establecido</string>
<string name="injected_context_turn_title">Modo de detección de giro</string>
<string name="injected_context_turn_title">Contexto del turno</string>
<string name="model_picker_cd_clear">Borrar</string>
<string name="model_picker_count">Modelos %d</string>
<string name="power_feature_back_desc">Atrás</string>
@@ -4300,4 +4300,5 @@
<string name="voice_overlay_setup_start">Iniciar ventana de voz</string>
<string name="voice_overlay_settings_hint">Opcional en ambas versiones. Abre el enfoque de voz en Chat y elige Superposición. Los permisos por sí solos nunca inician la escucha.</string>
<string name="voice_overlay_reset_position">Restablecer posición</string>
<string name="injected_context_gateway_unsupported">No se envía en el chat de Gateway. Esta conexión no admite contexto adicional por turno.</string>
</resources>
+5 -4
View File
@@ -801,7 +801,7 @@
<string name="chat_settings_notify_when_finishes">バックグラウンドのチャット通知</string>
<string name="chat_settings_notify_when_finishes_desc">バックグラウンドで Hermes が入力を必要としたとき、または完了したときに通知します</string>
<string name="chat_settings_share_phone_status">電話のステータスをエージェントと共有する</string>
<string name="chat_settings_share_phone_status_desc">すべてのチャット ターンにアプリと電話に関する短いシステム メッセージを含めます</string>
<string name="chat_settings_share_phone_status_desc">API のみのチャットにアプリと端末のコンテキストを含めます。標準の Gateway チャットでは送信されません。</string>
<string name="chat_settings_bridge_permissions">Bridge + 権限</string>
<string name="chat_settings_bridge_permissions_desc">アクセシビリティ、画面キャプチャ、オーバーレイ、通知が許可されているかどうか</string>
<string name="chat_settings_foreground_app">フォアグラウンドアプリ</string>
@@ -810,7 +810,7 @@
<string name="chat_settings_battery_level_desc">現在のバッテリーのパーセント。デフォルトではオフです。</string>
<string name="chat_settings_safety_rails">安全レール</string>
<string name="chat_settings_safety_rails_desc">ブロックリストの数、破壊的な動詞の数、および自動無効化タイマー</string>
<string name="chat_settings_preview">プレビュー</string>
<string name="chat_settings_preview">API のみのチャットの例</string>
<string name="chat_settings_no_system_message">(システムメッセージは送信されません)</string>
<string name="chat_settings_parse_tool_annotations">解析ツールの注釈</string>
<string name="chat_settings_experimental">実験的</string>
@@ -3376,10 +3376,10 @@
<string name="injected_context_phone_status_title">電話のステータス</string>
<string name="injected_context_relay_not_set">Relayが設定されていません</string>
<string name="injected_context_relay_title">Relay</string>
<string name="injected_context_subtitle">次のメッセージで送信されるコンテキスト</string>
<string name="injected_context_subtitle">このチャットのプレビューです。サーバー側のコンテキストは別途設定されます。</string>
<string name="injected_context_title">挿入されたコンテキスト</string>
<string name="injected_context_turn_not_set">未設定</string>
<string name="injected_context_turn_title">回転検出モード</string>
<string name="injected_context_turn_title">ターンのコンテキスト</string>
<string name="model_picker_cd_clear">クリア</string>
<string name="model_picker_count">%d モデル</string>
<string name="power_feature_back_desc">戻る</string>
@@ -4604,4 +4604,5 @@
<string name="voice_overlay_setup_start">音声オーバーレイを開始</string>
<string name="voice_overlay_settings_hint">両方のビルドで任意に利用できます。チャットの音声フォーカスでオーバーレイを選択します。権限の付与だけで録音は始まりません。</string>
<string name="voice_overlay_reset_position">位置をリセット</string>
<string name="injected_context_gateway_unsupported">Gateway チャットでは送信されません。この接続はターンごとの追加コンテキストに対応していません。</string>
</resources>
+5 -4
View File
@@ -778,7 +778,7 @@
<string name="chat_settings_notify_when_finishes">Уведомления о фоновом чате</string>
<string name="chat_settings_notify_when_finishes_desc">Уведомлять, когда Гермес требует ввода или заканчивает работу, когда приложение находится в фоновом режиме</string>
<string name="chat_settings_share_phone_status">Поделиться статусом телефона с агентом</string>
<string name="chat_settings_share_phone_status_desc">Включите короткое системное сообщение о приложении и телефоне в каждом обороте чата</string>
<string name="chat_settings_share_phone_status_desc">Добавлять контекст приложения и телефона в чаты только через API. В стандартных чатах Gateway он не отправляется.</string>
<string name="chat_settings_bridge_permissions">Мост + разрешения</string>
<string name="chat_settings_bridge_permissions_desc">Разрешения на доступность, захват экрана, наложение, уведомления</string>
<string name="chat_settings_foreground_app">Приложение на переднем плане</string>
@@ -787,7 +787,7 @@
<string name="chat_settings_battery_level_desc">Текущий процент заряда батареи. По умолчанию выключено.</string>
<string name="chat_settings_safety_rails">Ограждения безопасности</string>
<string name="chat_settings_safety_rails_desc">Количество в черном списке, количество деструктивных глаголов и таймер автоматического отключения</string>
<string name="chat_settings_preview">Предпросмотр</string>
<string name="chat_settings_preview">Пример чата только через API</string>
<string name="chat_settings_no_system_message">(системное сообщение не будет отправлено)</string>
<string name="chat_settings_parse_tool_annotations">Анализ аннотаций инструментов</string>
<string name="chat_settings_experimental">Экспериментальные</string>
@@ -3239,10 +3239,10 @@
<string name="injected_context_phone_status_title">Статус телефона</string>
<string name="injected_context_relay_not_set">плагин Relay не настроен</string>
<string name="injected_context_relay_title">плагин Relay</string>
<string name="injected_context_subtitle">Контекст отправляется с следующим сообщением</string>
<string name="injected_context_subtitle">Предпросмотр для этого чата. Контекст на стороне сервера настраивается отдельно.</string>
<string name="injected_context_title">Внедренный контекст</string>
<string name="injected_context_turn_not_set">Не установлено</string>
<string name="injected_context_turn_title">Режим обнаружения Turn</string>
<string name="injected_context_turn_title">Контекст хода</string>
<string name="model_picker_cd_clear">Очистить</string>
<string name="model_picker_count">%d моделей</string>
<string name="power_feature_back_desc">Назад</string>
@@ -4348,4 +4348,5 @@
<string name="voice_overlay_setup_start">Начать голосовой оверлей</string>
<string name="voice_overlay_settings_hint">Необязательно в обеих сборках. Откройте голосовой фокус в чате и выберите оверлей. Одни разрешения никогда не включают прослушивание.</string>
<string name="voice_overlay_reset_position">Сбросить положение</string>
<string name="injected_context_gateway_unsupported">Не отправляется в чате Gateway. Это подключение не поддерживает дополнительный контекст для каждого хода.</string>
</resources>
+5 -4
View File
@@ -887,7 +887,7 @@
<string name="chat_settings_notify_when_finishes">Background chat alerts</string>
<string name="chat_settings_notify_when_finishes_desc">Notify when Hermes needs input or finishes while the app is in the background</string>
<string name="chat_settings_share_phone_status">Share phone status with agent</string>
<string name="chat_settings_share_phone_status_desc">Include a short system message about the app and phone on every chat turn</string>
<string name="chat_settings_share_phone_status_desc">Include app and phone context in API-only chats. Not sent in standard Gateway chats.</string>
<string name="chat_settings_bridge_permissions">Bridge + permissions</string>
<string name="chat_settings_bridge_permissions_desc">Whether accessibility, screen capture, overlay, notifications are granted</string>
<string name="chat_settings_foreground_app">Foreground app</string>
@@ -896,7 +896,7 @@
<string name="chat_settings_battery_level_desc">Current battery percent. Off by default.</string>
<string name="chat_settings_safety_rails">Safety rails</string>
<string name="chat_settings_safety_rails_desc">Blocklist count, destructive-verb count, and auto-disable timer</string>
<string name="chat_settings_preview">Preview</string>
<string name="chat_settings_preview">Example for API-only chat</string>
<string name="chat_settings_no_system_message">(no system message will be sent)</string>
<string name="chat_settings_parse_tool_annotations">Parse tool annotations</string>
<string name="chat_settings_experimental">Experimental</string>
@@ -3769,10 +3769,10 @@
<string name="injected_context_phone_status_title">Phone status</string>
<string name="injected_context_relay_not_set">No relay configured</string>
<string name="injected_context_relay_title">Relay</string>
<string name="injected_context_subtitle">Context sent with next message</string>
<string name="injected_context_subtitle">Preview for this chat. Server-side context is configured separately.</string>
<string name="injected_context_title">Injected context</string>
<string name="injected_context_turn_not_set">Not set</string>
<string name="injected_context_turn_title">Turn detection mode</string>
<string name="injected_context_turn_title">Turn context</string>
<string name="model_picker_cd_clear">Clear</string>
<string name="model_picker_count">%d models</string>
<string name="power_feature_back_desc">Back</string>
@@ -4667,4 +4667,5 @@
<string name="voice_overlay_setup_start">Start voice overlay</string>
<string name="voice_overlay_settings_hint">Optional in both builds. Open Voice Focus in Chat and choose Overlay to start. Permissions alone never start listening.</string>
<string name="voice_overlay_reset_position">Reset position</string>
<string name="injected_context_gateway_unsupported">Not sent in Gateway chat. This connection does not support extra per-turn context.</string>
</resources>
@@ -0,0 +1,52 @@
package com.hermesandroid.relay.screenshots
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.captureRoboImage
import com.hermesandroid.relay.ui.components.InjectedContextSheet
import com.hermesandroid.relay.ui.theme.HermesRelayTheme
import com.hermesandroid.relay.viewmodel.ChatViewModel
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [34], qualifiers = "w360dp-h720dp-xhdpi")
class InjectedContextSheetTest {
@get:Rule val compose = createComposeRule()
@Test
fun gatewayPreviewLabelsUnsupportedContextWithoutShowingPhonePreamble() {
showPreview("gateway")
compose.onAllNodesWithText(
"Not sent in Gateway chat. This connection does not support extra per-turn context.",
).assertCountEquals(2)
compose.onNodeWithText("Hermes-Relay Android app", substring = true).assertDoesNotExist()
compose.onNodeWithText("Server-side persona").assertExists()
compose.onRoot().captureRoboImage("build/ui-regression/injected-context-gateway.png")
}
@Test
fun apiOnlyPreviewShowsThePreparedMobileContext() {
showPreview("sessions")
compose.onNodeWithText("Hermes-Relay Android app", substring = true).assertExists()
compose.onNodeWithText("Not sent in Gateway chat", substring = true).assertDoesNotExist()
compose.onRoot().captureRoboImage("build/ui-regression/injected-context-api.png")
}
private fun showPreview(endpoint: String) {
val preview = ChatViewModel().apply { streamingEndpoint = endpoint }.previewInjectedContext()
compose.setContent {
HermesRelayTheme(appThemeId = "hermes-relay", themePreference = "dark") {
InjectedContextSheet(context = preview, onDismiss = {})
}
}
}
}
@@ -1736,6 +1736,24 @@ class ChatViewModelGatewayInboundTurnTest {
assertEquals(false, gatewayClient.sessionModelProvider()?.fast)
}
@Test
fun injectedContextPreviewMatchesBareGatewayPayload() {
viewModel.appContextSettings = com.hermesandroid.relay.util.AppContextSettings(
master = true, battery = true, currentApp = true,
)
val preview = viewModel.previewInjectedContext()
assertFalse(preview.perTurnContextSupported)
assertNull(preview.combinedSystemMessage)
viewModel.sendMessage("Keep this message unchanged")
gatewayHarness.awaitRpc("prompt.submit")
val submitted = gatewayHarness.rpcLog.last { it.first == "prompt.submit" }.second
assertEquals(JsonPrimitive("Keep this message unchanged"), submitted["text"])
assertFalse(submitted.containsKey("system_message"))
assertFalse(submitted.containsKey("surface"))
assertEquals(0, apiCompletionsRequestCount.get())
}
@Test
fun dashboardOnlyConnectionCanSendWithoutApiClient() {
viewModel.updateGatewayClient(null)
@@ -0,0 +1,63 @@
package com.hermesandroid.relay.viewmodel
import com.hermesandroid.relay.data.Profile
import com.hermesandroid.relay.util.AppContextSettings
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class InjectedContextTest {
@Test
fun gatewayPreviewExcludesPhoneContextEvenWhenSharingIsEnabled() {
val viewModel = ChatViewModel().apply {
streamingEndpoint = "gateway"
appContextSettings = AppContextSettings(master = true, battery = true, currentApp = true)
setSelectedProfileProvider { Profile(name = "writer", model = "test-model", systemMessage = "Profile persona") }
}
val preview = viewModel.previewInjectedContext()
assertFalse(preview.perTurnContextSupported)
assertTrue(preview.personaOwnedServerSide)
assertNull(preview.personaPrompt)
assertNull(preview.appContext)
assertNull(preview.interfaceContext)
assertNull(preview.mediaCapability)
assertNull(preview.combinedSystemMessage)
}
@Test
fun apiOnlyTransportsRetainOptedInPhoneContextAndMasterOffRemovesIt() {
for (endpoint in listOf("sessions", "runs", "completions")) {
val viewModel = ChatViewModel().apply { streamingEndpoint = endpoint }
val enabled = viewModel.previewInjectedContext()
assertTrue(enabled.perTurnContextSupported)
assertFalse(enabled.personaOwnedServerSide)
assertTrue(enabled.appContext!!.contains("Hermes-Relay Android app"))
assertEquals(enabled.appContext, enabled.combinedSystemMessage)
viewModel.appContextSettings = AppContextSettings(master = false, battery = true, currentApp = true)
val disabled = viewModel.previewInjectedContext()
assertNull(disabled.appContext)
assertNull(disabled.combinedSystemMessage)
}
}
@Test
fun changingTransportRebuildsPreviewWithoutChangingSharingPreference() {
val viewModel = ChatViewModel().apply { streamingEndpoint = "sessions" }
val apiPreview = viewModel.previewInjectedContext()
viewModel.streamingEndpoint = "gateway"
assertNull(viewModel.previewInjectedContext().appContext)
assertTrue(viewModel.appContextSettings.master)
viewModel.streamingEndpoint = "sessions"
assertEquals(apiPreview.appContext, viewModel.previewInjectedContext().appContext)
}
}
+6 -6
View File
@@ -13,7 +13,7 @@
"verification": "ai-translated",
"review_refs": [],
"source_sha256": {
"main": "1059097e479afcdc8414c3f2df2ccffa9447c2a601dc408ba87cbce661e1e5a6",
"main": "8588c42385e783a2ddf888b515ca6b97a5df44a30d3619c048d3d03d91dd5278",
"sideload": "4abff4f1069091ec2de735c3037a7ec7d77699cb4321e8511a622437bceaf7c2"
},
"surfaces": {
@@ -48,7 +48,7 @@
"verification": "ai-translated",
"review_refs": [],
"source_sha256": {
"main": "1059097e479afcdc8414c3f2df2ccffa9447c2a601dc408ba87cbce661e1e5a6",
"main": "8588c42385e783a2ddf888b515ca6b97a5df44a30d3619c048d3d03d91dd5278",
"sideload": "4abff4f1069091ec2de735c3037a7ec7d77699cb4321e8511a622437bceaf7c2"
},
"surfaces": {
@@ -72,7 +72,7 @@
"verification": "ai-translated",
"review_refs": [],
"source_sha256": {
"main": "1059097e479afcdc8414c3f2df2ccffa9447c2a601dc408ba87cbce661e1e5a6",
"main": "8588c42385e783a2ddf888b515ca6b97a5df44a30d3619c048d3d03d91dd5278",
"sideload": "4abff4f1069091ec2de735c3037a7ec7d77699cb4321e8511a622437bceaf7c2"
},
"surfaces": {
@@ -96,7 +96,7 @@
"verification": "ai-translated",
"review_refs": [],
"source_sha256": {
"main": "1059097e479afcdc8414c3f2df2ccffa9447c2a601dc408ba87cbce661e1e5a6",
"main": "8588c42385e783a2ddf888b515ca6b97a5df44a30d3619c048d3d03d91dd5278",
"sideload": "4abff4f1069091ec2de735c3037a7ec7d77699cb4321e8511a622437bceaf7c2"
},
"surfaces": {
@@ -120,7 +120,7 @@
"verification": "ai-translated",
"review_refs": [],
"source_sha256": {
"main": "1059097e479afcdc8414c3f2df2ccffa9447c2a601dc408ba87cbce661e1e5a6",
"main": "8588c42385e783a2ddf888b515ca6b97a5df44a30d3619c048d3d03d91dd5278",
"sideload": "4abff4f1069091ec2de735c3037a7ec7d77699cb4321e8511a622437bceaf7c2"
},
"surfaces": {
@@ -135,7 +135,7 @@
"verification": "ai-translated",
"review_refs": [],
"source_sha256": {
"main": "1059097e479afcdc8414c3f2df2ccffa9447c2a601dc408ba87cbce661e1e5a6",
"main": "8588c42385e783a2ddf888b515ca6b97a5df44a30d3619c048d3d03d91dd5278",
"sideload": "4abff4f1069091ec2de735c3037a7ec7d77699cb4321e8511a622437bceaf7c2"
},
"surfaces": {
+27
View File
@@ -58,6 +58,33 @@ standard upstream media/file delivery, and concise-response expectations. Once
that contract is available in the supported Hermes baseline, adopt it and add
Gateway conformance coverage proving the exact prompt bytes and session source.
Issues [#556](https://github.com/Codename-11/hermes-relay/issues/556) and
[#557](https://github.com/Codename-11/hermes-relay/issues/557) share this contract
gap. The Android audit fix labels unsupported context; automatic Android
identification and Gateway phone-status delivery still require upstream work.
Rechecked against upstream `5dea46d13deec9549bdc2ea703ae9201d733c28d`:
- [`methods_prompt.py`](https://github.com/NousResearch/hermes-agent/blob/5dea46d13deec9549bdc2ea703ae9201d733c28d/tui_gateway/methods_prompt.py)
accepts `surface` only for `hud` and `voice-live`; it has no general
per-turn system-context parameter. Neither surface means Android.
- [`session_notifications.py`](https://github.com/NousResearch/hermes-agent/blob/5dea46d13deec9549bdc2ea703ae9201d733c28d/tui_gateway/session_notifications.py)
adds those built-in surface notes to model input without changing the
persisted user row. This is the existing upstream seam to extend, with
explicit client capability negotiation, bounded opted-in context, and
turn-owned snapshots through queueing, retries, and client switches.
- [`server.py`](https://github.com/NousResearch/hermes-agent/blob/5dea46d13deec9549bdc2ea703ae9201d733c28d/tui_gateway/server.py)
accepts a caller-supplied session `source`, but
[`prompt_builder.py`](https://github.com/NousResearch/hermes-agent/blob/5dea46d13deec9549bdc2ea703ae9201d733c28d/agent/prompt_builder.py)
has no Android platform hint. A session-origin label is not verified
current-turn sender identity. Do not invent a platform or use a client hint
as authorization for phone tools.
- The API server's `POST /api/sessions` source field belongs to the explicit
API-only surface. Android standard chat creates/resumes sessions through
Gateway RPC; that REST field does not add per-turn context to `prompt.submit`.
Keep unknown/older hosts supported without sending private parameters,
rewriting the user transcript, overriding persona, or requiring Relay.
---
## Scope sensitive-media prompt guidance to capable clients
+2
View File
@@ -597,6 +597,8 @@ Bottom navigation bar with 4 tabs:
```
### Chat Tab
- **Context preview** — the Injected context sheet describes the bound chat transport, not a delivery receipt. Standard Gateway chat has no general per-turn system-context slot: phone status, the Android client preamble, media hints, and interface-context blocks are omitted. Phone status and turn context show an explicit unsupported note. API-only chats retain the opted-in context and sharing controls; the Settings example is labeled API-only. Gateway persona and optional Relay-reported configuration remain separately server-owned. No context is prepended to user text or written into the personality slot to bypass this boundary.
- **Top bar and Profile Shelf (three-layer agent model).** Layout from left to right:
1. **Connection chip** — tap to open `ConnectionSwitcherSheet` (all paired servers + health indicator). Auto-hidden when you only have one Connection. See `docs/decisions.md` §19.
2. **Agent avatar/name region** — tap to expand or collapse the Profile Shelf immediately below the app bar. With only one visible effective identity, the shelf stays hidden and the same tap opens Agent Passport.
+3
View File
@@ -21,6 +21,9 @@ import sys
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
FOCUSED_TESTS = (
"com.hermesandroid.relay.viewmodel.InjectedContextTest",
"com.hermesandroid.relay.screenshots.InjectedContextSheetTest",
"com.hermesandroid.relay.viewmodel.ChatViewModelGatewayInboundTurnTest.injectedContextPreviewMatchesBareGatewayPayload",
"com.hermesandroid.relay.voice.VoiceOverlayLifecycleTest",
"com.hermesandroid.relay.voice.VoiceOverlayForegroundServiceTest",
"com.hermesandroid.relay.voice.VoiceOverlayPresentationTest",
+1 -1
View File
@@ -121,7 +121,7 @@ Google Play builds do not include AccessibilityService-backed screen reading or
| Chat empty state | Logo + suggestion chips |
| Animated streaming dots | Pulsing 3-dot indicator during streaming |
| Haptic feedback | On send, copy, stream complete, error |
| App context prompt | Toggleable system message for mobile context |
| App context prompt | Opt-in phone details and mobile context for API-only chats; Gateway limitations shown in the context preview |
## Security
+16 -9
View File
@@ -255,15 +255,22 @@ Each assistant message shows token usage below the timestamp:
## App context prompt
When enabled (**Settings → Chat → App context prompt**, on by default),
Hermes-Relay tells the agent it's talking to a phone so replies stay
mobile-friendly and concise, and can attach optional bridge/permission and
safety-rail summaries. On the standard (API-server) connection this rides an
invisible system message. The Gateway connection carries no app-context preamble
— its protocol has no hidden per-turn slot, and adding one would leave the text
in your saved chat history — so there the agent reads phone state on demand via
the `android_phone_status` tool. Privacy-sensitive fields (foreground app,
battery) default off and are only added when you opt in.
**Settings → Chat → Share phone status with agent** controls an extra system
message in API-only chats. It identifies Hermes-Relay Android to the agent and
can include optional phone status. Foreground app and battery sharing default
off and are included only when you opt in. Turning the master switch off also
removes the Android preamble.
Standard Gateway chats do not send this block: upstream has no general
per-turn context slot. The **Injected context** sheet marks phone status and
turn context as unsupported on Gateway, and Settings labels its sample as an
API-only example. The sheet is a preview, not proof of delivery or a complete
view of the agent prompt; persona and Relay configuration are server-owned.
The optional Relay `android_phone_status` tool can provide phone state when
enabled for the selected profile. Its presence does not identify who sent the
current message. Automatic Android identification on Gateway still needs an
upstream client-surface contract; pairing alone does not provide it.
## Persistent connection
+1 -1
View File
@@ -89,7 +89,7 @@ Available in **Settings > Chat**.
|---------|---------|-------------|
| Show reasoning | `true` | Display thinking/reasoning blocks above responses |
| Show token usage | `true` | Display input/output token counts and estimated cost |
| App context prompt | `true` | Send system message telling agent user is on mobile |
| Share phone status with agent | `true` | Include mobile context in API-only chats; not sent in standard Gateway chats |
| Tool call display | `Detailed` | How tool calls appear: Off, Compact, or Detailed |
| Personality | Server default | Active personality from `config.agent.personalities` via `GET /api/config` |