Add used/full context meter to chat screen (#717)
* Adopt /api/health REST route (issue #713) Cheap liveness probe on the connection screen: GET /api/health surfaces backend version + auth mode (Token vs Gated/OAuth) without the heavier /api/status payload. Added HermesApiService.getHealth + HealthStatus model; ConnectViewModel.loadHealth probes on open (best-effort, silent on failure); ConnectScreen shows a read-only health card when reachable. Verified: ktlint clean, assembleDebug BUILD SUCCESSFUL locally. * Add used/full context meter to chat screen Shows session prompt tokens vs model context window above the composer, polled every 5s. Color-coded bar warns at 70%/90%. * fix(chat): fix unit tests for context usage meter Format >=100k tokens cleanly in ContextUsageChip and stub AuthManager profile ID in ChatViewModelTest. * Fix context meter: source used tokens from input_tokens Gateway /api/sessions/{id} has no last_prompt_tokens column; use input_tokens instead. Show meter as soon as full context is known. * Add tappable context breakdown sheet Chip opens a bottom sheet with input/output/cache/read/write/reasoning token rows + message count. Source breakdown from /api/sessions/{id}. * Refresh context meter on streaming complete Trigger fetchContextUsage immediately when a message finishes streaming instead of waiting for the 5s session-sync poll. * Fix context meter: source used tokens from live WS session.info The REST /api/sessions/{id} endpoint reads the DB input_tokens column, which is stale/0 for active sessions — that's why the meter was stuck at 0. Switch the meter to the live values the gateway pushes over the WS session.info payload after each turn: - used = last_prompt_tokens (real used context for the turn) - full = context_length (model-accurate window; keep /api/model/info as a fallback before the first turn completes) fetchContextUsage now only tops up the denominator from /api/model/info and no longer overwrites the live WS value with the stale REST input_tokens. The 5s poll remains as a fallback. * Revert "Fix context meter: source used tokens from live WS session.info" This reverts commit 932393fd80993d57e66fe93552a949460d646674. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
c181d1d593
commit
0180fb12c2
15 files changed
+641
-2
No files matched your search
@@ -35,6 +35,7 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -316,6 +317,43 @@ fun ConnectScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Backend health read-out (issue #713): cheap /api/health probe.
|
||||
// Shows version + auth mode when reachable; nothing when null.
|
||||
state.health?.let { health ->
|
||||
val authMode =
|
||||
if (health.authRequired == true) {
|
||||
stringResource(R.string.connect_health_auth_gated)
|
||||
} else {
|
||||
stringResource(R.string.connect_health_auth_token)
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.connect_health_title),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
R.string.connect_health_body,
|
||||
health.version ?: "?",
|
||||
authMode,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.config.ConnectionProfile
|
||||
import com.m57.hermescontrol.data.config.resolveBaseUrl
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.model.HealthStatus
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.CleartextPolicy
|
||||
import com.m57.hermescontrol.data.remote.NetworkError
|
||||
@@ -33,6 +34,7 @@ data class ConnectUiState(
|
||||
val saveProfile: Boolean = false,
|
||||
val profiles: List<ConnectionProfile> = emptyList(),
|
||||
val selectedProfile: ConnectionProfile? = null,
|
||||
val health: HealthStatus? = null,
|
||||
)
|
||||
|
||||
class ConnectViewModel(
|
||||
@@ -60,6 +62,29 @@ class ConnectViewModel(
|
||||
profileName = selectedProfile?.name ?: "",
|
||||
)
|
||||
}
|
||||
loadHealth()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cheap liveness probe (issue #713): GET /api/health against the current
|
||||
* URL/token. Surfaces backend version + auth mode on the connection screen
|
||||
* without the heavier /api/status payload. Best-effort: failures are silent
|
||||
* (health stays null and the screen simply shows no read-out).
|
||||
*/
|
||||
fun loadHealth() {
|
||||
val state = _uiState.value
|
||||
val endpoint = runCatching { ServerEndpoint.parseForBuild(state.baseUrl) }.getOrNull()
|
||||
if (endpoint == null || state.token.isBlank()) return
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
val tempApi = ApiClient.createTempService(endpoint.baseUrl.toString(), state.token)
|
||||
safeApiCall { tempApi.getHealth() }
|
||||
}
|
||||
if (result is NetworkResult.Success) {
|
||||
_uiState.update { it.copy(health = result.data) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onProfileNameChange(value: String) {
|
||||
|
||||
Reference in new issue
Block a user