feat(workstate): read-model live-state minimal des conversations/délégations (Lot A backend)

Introduit le module application `workstate` (modèle de read-model live + snapshots
des conversations/délégations en cours) et l'expose via la couche terminal
(exports mod/registry). Câble la surface Tauri : DTO, commande et state pour
exposer le live-state au frontend (lib + state + commands), avec tests.

QA verte. Réserve environnementale non bloquante : tests loopback socket Unix
réels non exécutables en sandbox (UnixListener::bind PermissionDenied),
alternatives avec skips vertes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:06:22 +02:00
parent 6cfa0b04c6
commit aae18499a9
10 changed files with 663 additions and 28 deletions

View File

@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
use application::{
AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport,
LayoutKind, ListProjectsOutput, OpenProjectOutput,
LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput, ProjectWorkState,
};
use domain::{Project, ProjectId};
use domain::{AgentBusyState, Project, ProjectId};
/// Request DTO for the `health` command.
#[derive(Debug, Clone, Default, Deserialize)]
@ -1472,6 +1472,83 @@ impl LiveAgentListDto {
}
}
/// Runtime family of a live work-state session.
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum LiveWorkSessionKindDto {
/// Raw PTY-backed CLI session.
Pty,
/// Structured agent-session backend.
Structured,
}
impl From<LiveSessionKind> for LiveWorkSessionKindDto {
fn from(kind: LiveSessionKind) -> Self {
match kind {
LiveSessionKind::Pty => Self::Pty,
LiveSessionKind::Structured => Self::Structured,
}
}
}
/// Live session coordinates in the project work-state read model.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LiveWorkSessionDto {
/// The layout node currently hosting the session view.
pub node_id: String,
/// The live session id.
pub session_id: String,
/// Runtime family that owns the session.
pub kind: LiveWorkSessionKindDto,
}
/// One manifest agent's current live/busy state.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentWorkStateDto {
/// Agent id.
pub agent_id: String,
/// Agent display name.
pub name: String,
/// Runtime profile id assigned to the agent.
pub profile_id: String,
/// Live session, if any.
pub live: Option<LiveWorkSessionDto>,
/// Current mediated-input busy state.
pub busy: AgentBusyState,
}
/// Project-level read model for conversation/delegation UX.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectWorkStateDto {
/// Manifest agents in manifest order, enriched with live/busy state.
pub agents: Vec<AgentWorkStateDto>,
}
impl From<ProjectWorkState> for ProjectWorkStateDto {
fn from(state: ProjectWorkState) -> Self {
Self {
agents: state
.agents
.into_iter()
.map(|agent| AgentWorkStateDto {
agent_id: agent.agent_id.to_string(),
name: agent.name,
profile_id: agent.profile_id.to_string(),
live: agent.live.map(|live| LiveWorkSessionDto {
node_id: live.node_id.to_string(),
session_id: live.session_id.to_string(),
kind: live.kind.into(),
}),
busy: agent.busy,
})
.collect(),
}
}
}
/// Request DTO for `attach_live_agent`: bind an already-running agent session to
/// a visible layout cell without spawning a new process.
#[derive(Debug, Clone, Deserialize)]