feat(chat): /stop also kills background processes like desktop (#1409)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 14:33:07 +03:00
1 parent 05519ba1ff
commit 0e39a71591
8 files changed
+133 -4

No files matched your search

@@ -94,6 +94,9 @@ object WsMethods {
/** Kill a single background process (scoped to the active session). */
const val PROCESS_KILL = "process.kill"
/** Kill every background process in the gateway registry (`/stop`, global). */
const val PROCESS_STOP = "process.stop"
// ── Subagents (issue #1089) ───────────────────────────────────────────
/** List active subagents and delegations for a session. */
@@ -0,0 +1,15 @@
package com.m57.hermescontrol.data.ws.contract
import kotlinx.serialization.Serializable
/**
* `process.stop` kills every background process in the gateway registry (the desktop `/stop` second half). Desktop
* sends `{}`: the backend call is global, so no `session_id` or `profile` override is sent.
*/
@Serializable
data object ProcessStopParams
@Serializable
data class ProcessStopResult(
val killed: Int? = null,
)
@@ -363,6 +363,13 @@ object RpcMethods {
JsonElement.serializer(),
)
val PROCESS_STOP: RpcMethod<ProcessStopParams, ProcessStopResult> =
RpcMethod(
WsMethods.PROCESS_STOP,
ProcessStopParams.serializer(),
ProcessStopResult.serializer(),
)
// Passthrough JsonElement results: callers keep their existing legacy decoders.
val PLUGINS_MANAGE: RpcMethod<PluginsManageParams, JsonElement> =
RpcMethod(
@@ -514,6 +521,7 @@ object RpcMethods {
SUBAGENT_TAIL,
PROCESS_LIST,
PROCESS_KILL,
PROCESS_STOP,
PLUGINS_MANAGE,
PROJECTS_LIST,
PROFILES_LIST,
@@ -43,6 +43,7 @@ import com.m57.hermescontrol.data.ws.contract.ConfigGetParams
import com.m57.hermescontrol.data.ws.contract.ConfigSetParams
import com.m57.hermescontrol.data.ws.contract.FileAttachParams
import com.m57.hermescontrol.data.ws.contract.ImageAttachBytesParams
import com.m57.hermescontrol.data.ws.contract.ProcessStopParams
import com.m57.hermescontrol.data.ws.contract.PromptBtwParams
import com.m57.hermescontrol.data.ws.contract.RpcMethod
import com.m57.hermescontrol.data.ws.contract.RpcMethods
@@ -3716,6 +3717,10 @@ class ChatViewModel(
interruptSession()
}
is SlashResult.Stop -> {
stopSessionAndProcesses()
}
is SlashResult.NewSession -> {
val currentTitle = _uiState.value.chatTitle
if (currentTitle.equals("Bot Chat", ignoreCase = true)) {
@@ -4344,6 +4349,28 @@ class ChatViewModel(
}
}
/**
* `/stop`, matching the desktop app: interrupt the active turn (same path as the composer Stop button), then
* kill every background process via `process.stop`. The button stays interrupt-only.
*/
private fun stopSessionAndProcesses() {
interruptSession()
viewModelScope.launch(ioDispatcher) {
val message =
try {
val killed = wsClient.call(RpcMethods.PROCESS_STOP, ProcessStopParams).killed ?: 0
when {
killed > 0 -> "Stopped $killed background process${if (killed == 1) "" else "es"}."
else -> "No background processes to stop."
}
} catch (e: Exception) {
if (e is CancellationException) throw e
"Could not stop background processes: ${e.message ?: e.javaClass.simpleName}"
}
addSystemMessage(message)
}
}
/**
* Send course-correction guidance to a live subagent child session (issue #1030).
*/
@@ -27,7 +27,11 @@ class SlashCommandDispatcher {
val cmd = parts[0].lowercase()
return when (cmd) {
"/stop", "/interrupt" -> {
"/stop" -> {
SlashResult.Stop
}
"/interrupt" -> {
SlashResult.Interrupt
}
@@ -96,6 +100,9 @@ sealed class SlashResult {
/** Interrupt the active session (client-side immediate). */
data object Interrupt : SlashResult()
/** Desktop `/stop`: interrupt the active turn, then kill background processes (`process.stop`). */
data object Stop : SlashResult()
/** Create a new session (client-side immediate). */
data object NewSession : SlashResult()