feat(chat): livre la CLI custom de chat agent (#147) et corrige Cancel
Implémente la vue chat structurée par cellule agent (toggle TUI/CLI custom, préférence persistée `preferred_view`, reattach live, composer + pièces jointes) avec le socle backend AgentSession/ChatBridge (UserPrompt, cancel_current_turn, routage interrupt_agent, commande cancel_agent_chat). Corrige le bug bloquant relevé par QA : le bouton Cancel de CustomAgentChatView interrompait tout le tour via closeAgentChat au lieu de n'annuler que le tour courant via cancelAgentChat, ce qui tuait la session contrairement au contrat produit validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -22,6 +22,23 @@ pub enum Direction {
|
||||
Column,
|
||||
}
|
||||
|
||||
/// Preferred live view for an agent leaf.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PreferredView {
|
||||
/// Native terminal/TUI view.
|
||||
#[default]
|
||||
Tui,
|
||||
/// Structured chat view.
|
||||
Chat,
|
||||
}
|
||||
|
||||
/// Returns `true` when the preferred view is the default `Tui`.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_default_preferred_view(view: &PreferredView) -> bool {
|
||||
*view == PreferredView::Tui
|
||||
}
|
||||
|
||||
/// Returns `true` when a boolean is `false`. Used as a `skip_serializing_if`
|
||||
/// predicate so that default (`false`) flags are omitted from the serialized
|
||||
/// form, preserving backward/forward compatibility with leaves that predate the
|
||||
@ -64,6 +81,10 @@ pub struct LeafCell {
|
||||
/// last closed. Used to decide whether to auto-resume the agent on reopen.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub agent_was_running: bool,
|
||||
/// Preferred live view for this cell. Additive and defaulted to TUI for old
|
||||
/// layouts; meaningful only when [`Self::agent`] is set.
|
||||
#[serde(default, skip_serializing_if = "is_default_preferred_view")]
|
||||
pub preferred_view: PreferredView,
|
||||
}
|
||||
|
||||
impl LeafCell {
|
||||
@ -81,6 +102,7 @@ impl LeafCell {
|
||||
conversation_id: None,
|
||||
engine_session_id: None,
|
||||
agent_was_running: false,
|
||||
preferred_view: PreferredView::Tui,
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +127,13 @@ impl LeafCell {
|
||||
self.engine_session_id = engine_session_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wither additif : pose la vue live préférée de la cellule.
|
||||
#[must_use]
|
||||
pub fn with_preferred_view(mut self, preferred_view: PreferredView) -> Self {
|
||||
self.preferred_view = preferred_view;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A weighted child within a [`SplitContainer`]. The `weight` is a *relative*
|
||||
@ -567,6 +596,37 @@ impl LayoutTree {
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
/// Sets the preferred live view on the leaf `target`.
|
||||
///
|
||||
/// Pure: returns a new validated tree.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`LayoutError::NodeNotFound`] if `target` is not a leaf in the tree.
|
||||
pub fn set_cell_preferred_view(
|
||||
&self,
|
||||
target: NodeId,
|
||||
preferred_view: PreferredView,
|
||||
) -> Result<Self, LayoutError> {
|
||||
let mut found = false;
|
||||
let root = map_node(&self.root, &mut |node| {
|
||||
if let LayoutNode::Leaf(leaf) = node {
|
||||
if leaf.id == target {
|
||||
found = true;
|
||||
let mut leaf = leaf.clone();
|
||||
leaf.preferred_view = preferred_view;
|
||||
return LayoutNode::Leaf(leaf);
|
||||
}
|
||||
}
|
||||
node.clone()
|
||||
});
|
||||
if !found {
|
||||
return Err(LayoutError::NodeNotFound(target));
|
||||
}
|
||||
let tree = Self { root };
|
||||
tree.validate()?;
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
/// Sets (or, with `None`, clears) the persistent CLI `conversation_id` on
|
||||
/// the leaf `target`.
|
||||
///
|
||||
|
||||
@ -195,8 +195,9 @@ pub use git::GitRepository;
|
||||
pub use layout::{
|
||||
CustomPluginLayoutCell, Direction, GridCell, GridContainer, LayoutError, LayoutNode,
|
||||
LayoutTree, LeafCell, PersistedMonitorState, PersistedPluginLayoutWindow, PersistedWindowKind,
|
||||
PersistedWindowPosition, PersistedWindowSize, PersistedWindowState, SplitContainer, Tab,
|
||||
WeightedChild, Window, WindowStateSnapshot, Workspace, WINDOW_STATE_SNAPSHOT_VERSION,
|
||||
PersistedWindowPosition, PersistedWindowSize, PersistedWindowState, PreferredView,
|
||||
SplitContainer, Tab, WeightedChild, Window, WindowStateSnapshot, Workspace,
|
||||
WINDOW_STATE_SNAPSHOT_VERSION,
|
||||
};
|
||||
|
||||
pub use events::{DomainEvent, OrchestrationSource};
|
||||
|
||||
@ -1200,6 +1200,19 @@ pub trait AgentSession: Send + Sync {
|
||||
self.send(prompt).await
|
||||
}
|
||||
|
||||
/// Interrupts the currently running turn without shutting down the session.
|
||||
///
|
||||
/// Default is a no-op so existing structured adapters/fakes remain valid.
|
||||
/// Process-backed adapters that retain a live child during `send` should
|
||||
/// override it and kill only that child.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AgentSessionError::Io`] when the adapter attempted a concrete cancel and
|
||||
/// it failed.
|
||||
async fn cancel_current_turn(&self) -> Result<(), AgentSessionError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Termine proprement la session (tue le process/SDK sous-jacent). Idempotent.
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
Reference in New Issue
Block a user