feat(#532): add process.list + process.kill manager screen (#556)

Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-11 20:17:03 +03:00
1 parent cf893585b1
commit afa82ceec2
10 files changed
+661

No files matched your search

@@ -0,0 +1,50 @@
package com.m57.hermescontrol.data.model
/**
* A single background process owned by a session, as returned by the gateway
* `process.list` RPC.
*
* Field names match the gateway JSON exactly (Gson deserializes by name,
* snake_case). See `tui_gateway/server.py::_session_processes` and
* `tools/process_registry.list_sessions` for the canonical shape.
*
* @property sessionId Stable process id used by `process.kill` as `process_id`.
* @property command First 200 chars of the spawned command line.
* @property cwd Working directory the process was launched in.
* @property pid OS process id (0 / null for detached or not-yet-spawned).
* @property startedAt ISO-8601 local timestamp string from the backend.
* @property uptimeSeconds Seconds since spawn (backend-computed).
* @property status `"running"` or `"exited"`.
* @property outputPreview Last 200 chars of buffered output.
* @property outputTail Last 4000 chars of buffered output (richer preview).
* @property exitCode Present only when [status] == `"exited"`.
* @property sessionScoped True when surfaced only because it shares the gateway
* session (not the current task) — a possibly-forgotten long-lived process.
*/
data class ProcessInfo(
val sessionId: String,
val command: String? = null,
val cwd: String? = null,
val pid: Int? = null,
val startedAt: String? = null,
val uptimeSeconds: Int? = null,
val status: String? = null,
val outputPreview: String? = null,
val outputTail: String? = null,
val exitCode: Int? = null,
val sessionScoped: Boolean = false,
) {
val isRunning: Boolean get() = status != "exited"
/** A short, human-readable title derived from the command line. */
val title: String
get() =
(command ?: "")
.lineSequence()
.firstOrNull()
?.trim()
.orEmpty()
.ifEmpty { "background process" }
companion object
}
@@ -0,0 +1,28 @@
package com.m57.hermescontrol.data.session
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* App-wide mirror of the active chat session id.
*
* The desktop `process.list` / `process.kill` RPCs are session-scoped — the
* gateway returns `4001 "session not found"` without a valid `session_id`, and
* mobile has no global "current session" holder (ChatViewModel keeps it
* privately). This singleton holds the last-known active session id so
* session-scoped drawer screens (e.g. the Processes screen, issue #532) can
* issue those RPCs. ChatViewModel writes here on every switch/resume; it is a
* best-effort mirror and may be null when no chat session has been opened yet.
*/
object ActiveSessionHolder {
private val _activeSessionId = MutableStateFlow<String?>(null)
/** The currently active chat session id, or null if none is known yet. */
val activeSessionId: StateFlow<String?> = _activeSessionId.asStateFlow()
/** Update the active session id. Pass null to clear (e.g. on logout). */
fun set(sessionId: String?) {
_activeSessionId.value = sessionId
}
}
@@ -32,4 +32,12 @@ object WsMethods {
/** Stage a non-image file (data URL) for agent access. */
const val FILE_ATTACH = "file.attach"
// ── Background process manager (issue #532) ──────────────────────────
/** List background processes owned by the active session. */
const val PROCESS_LIST = "process.list"
/** Kill a single background process (scoped to the active session). */
const val PROCESS_KILL = "process.kill"
}