feat(aiturk): hide what the paired IDE cannot serve, plain error sentences, reconnect on return

When paired with AITURK IDE:
- an "unknown method" (-32601) answer and the proxy's 403/route-404 are shown
  as plain sentences, never as raw JsonRpcError text; a method the IDE does
  not have is not sent again on the same socket
- Account connectors, Vault, Keys and Files are hidden (the IDE has no
  connectors.* / vault.* and its phone proxy refuses /api/env and /api/files)
- an idle-closed chat socket is reopened when the app returns to the
  foreground; idle tunnel connections are dropped on a network change
- an unsaved chat no longer polls /api/sessions/<id>
- the pairing screen shows the app version

A plain Hermes connection is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-05 17:21:32 +03:00
1 parent c2d828b90a
commit 827c74b3c3
18 files changed
+709 -22

No files matched your search

@@ -0,0 +1,60 @@
package com.m57.hermescontrol.ui.aiturkpair
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Computer
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.navigation3.runtime.NavKey
import com.m57.hermescontrol.AccountConnectorsScreen
import com.m57.hermescontrol.FilesScreen
import com.m57.hermescontrol.KeysScreen
import com.m57.hermescontrol.R
import com.m57.hermescontrol.SettingsVault
import com.m57.hermescontrol.data.aiturk.AiturkCompat
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.NavIcon
/**
* Screens a paired AITURK IDE cannot serve, hidden from the drawer and the
* settings list while that pairing is the active connection:
*
* - Account connectors, Credential vault: the IDE's agent has no
* `connectors.*` / `vault.*` methods;
* - Keys, Files: the IDE's phone proxy refuses `/api/env` and `/api/files`
* on purpose (secrets and the computer's files stay on the computer).
*
* A plain Hermes dashboard connection shows everything, as upstream does.
*/
object AiturkFeatureGate {
val HIDDEN_WHEN_LINKED: Set<NavKey> = setOf(AccountConnectorsScreen, KeysScreen, FilesScreen, SettingsVault)
fun shows(
key: NavKey,
linked: Boolean = AiturkCompat.isLinked(),
): Boolean = !(linked && key in HIDDEN_WHEN_LINKED)
}
/** Shown instead of a hidden screen that is still reached (restored back stack, shortcut). */
@Composable
fun AiturkUnavailableScreen(
@StringRes titleRes: Int,
onOpenDrawer: (() -> Unit)?,
) {
HermesScaffold(
title = { Text(stringResource(titleRes)) },
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
) { padding ->
EmptyState(
title = stringResource(R.string.aiturk_feature_unavailable),
subtitle = stringResource(R.string.aiturk_feature_unavailable_hint),
icon = Icons.Filled.Computer,
modifier = Modifier.padding(padding).testTag("aiturk_unavailable"),
)
}
}
@@ -43,6 +43,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
@@ -61,6 +62,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.aiturk.AiturkCompat
import com.m57.hermescontrol.data.aiturk.AiturkPairing
import com.m57.hermescontrol.data.remote.requiresLocalNetworkPermission
@@ -286,6 +288,15 @@ fun AiturkPairScreen(
TextButton(onClick = onBack, enabled = !state.busy) {
Text(stringResource(R.string.aiturk_pair_back))
}
// So the user can tell which build is installed when support asks.
Text(
text = stringResource(R.string.aiturk_app_version, AiturkCompat.versionLabel()),
modifier = Modifier.testTag("aiturk_app_version"),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
)
}
}
}