a3s 0.10.1

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use super::persistence::code_web_store_dir;
use super::*;

impl KernelService {
    pub(in crate::api::code_web) async fn session_status(
        &self,
        session_id: &str,
    ) -> BootResult<serde_json::Value> {
        let session = self.kernel_session(session_id).await?;
        let settings = self.session_settings_snapshot(session_id).await;
        let os_status = code_web_os_status(self.state.as_ref()).await?;
        let runtime_connected = os_status
            .get("runtimeToolActive")
            .and_then(Value::as_bool)
            .unwrap_or(false);
        Ok(json!({
            "sessionId": session.session_id(),
            "status": "idle",
            "cwd": session.workspace().display().to_string(),
            "model": self.effective_model(&settings),
            "followDefaultModel": settings.follow_default_model,
            "permissionMode": settings.permission_mode,
            "planningMode": settings.planning_mode,
            "goalTracking": settings.goal_tracking,
            "mcpServers": [],
            "runtime": {
                "connected": runtime_connected,
                "transport": "rest",
                "os": os_status,
            },
            "commands": ["clear", "compact", "cost", "help", "history", "mcp", "model", "tools"],
        }))
    }

    pub(in crate::api::code_web) async fn effort_levels(&self) -> BootResult<serde_json::Value> {
        Ok(json!({
            "items": effort_levels_json(),
        }))
    }

    pub(in crate::api::code_web) async fn session_controls(
        &self,
        session_id: &str,
    ) -> BootResult<serde_json::Value> {
        let session = self.kernel_session(session_id).await?;
        let controls = self.session_controls_snapshot(session_id).await;
        let settings = self.session_settings_snapshot(session_id).await;
        let model = self.effective_model(&settings);
        let context_limit = code_web_context_limit_for_model(self.state.as_ref(), model.as_deref());
        let context = self
            .controls_context_usage(session_id, session.as_ref(), context_limit)
            .await?;
        Ok(controls_json(
            session_id,
            &controls,
            &settings,
            &context,
            Some(context_limit),
            self.state.auto_compact_threshold,
        ))
    }

    pub(in crate::api::code_web) async fn update_session_controls(
        &self,
        session_id: &str,
        request: serde_json::Value,
    ) -> BootResult<serde_json::Value> {
        self.kernel_session(session_id).await?;
        let (controls_snapshot, effort_changed, goal_changed) = {
            let mut controls_by_session = self.state.session_controls.lock().await;
            let controls = controls_by_session
                .entry(session_id.to_string())
                .or_default();
            let original_effort = controls.effort.clone();
            let original_goal = controls.goal.clone();

            if let Some(effort_value) = request.get("effort") {
                let effort = effort_value
                    .as_str()
                    .ok_or_else(|| BootError::BadRequest("effort must be a string".to_string()))?;
                let profile = normalize_effort(effort).ok_or_else(|| {
                    BootError::BadRequest(format!("unsupported effort level `{effort}`"))
                })?;
                controls.effort = profile.id.to_string();
            }

            if let Some(goal_value) = request.get("goal") {
                match goal_value {
                    Value::Null => {
                        controls.goal = None;
                        controls.goal_run = None;
                    }
                    Value::String(goal) => {
                        let goal = normalize_goal(goal);
                        if controls.goal != goal {
                            controls.goal_run = goal.as_ref().map(|_| {
                                let now = chrono::Utc::now().timestamp_millis();
                                CodeWebGoalRun {
                                    started_at: now,
                                    updated_at: now,
                                    ..CodeWebGoalRun::default()
                                }
                            });
                        }
                        controls.goal = goal;
                    }
                    _ => {
                        return Err(BootError::BadRequest(
                            "goal must be a string or null".to_string(),
                        ));
                    }
                }
            }

            (
                controls.clone(),
                controls.effort != original_effort,
                controls.goal != original_goal,
            )
        };

        if goal_changed {
            self.state
                .session_settings
                .lock()
                .await
                .entry(session_id.to_string())
                .or_default()
                .goal_tracking = Some(controls_snapshot.goal.is_some());
        }

        if effort_changed || goal_changed {
            let settings = self.session_settings_snapshot(session_id).await;
            self.rebuild_session_with_settings(session_id, &settings)
                .await?;
        }

        self.persist_session_state(session_id).await?;

        let settings = self.session_settings_snapshot(session_id).await;
        let model = self.effective_model(&settings);
        let context_limit = code_web_context_limit_for_model(self.state.as_ref(), model.as_deref());
        let session = self.kernel_session(session_id).await?;
        let context = self
            .controls_context_usage(session_id, session.as_ref(), context_limit)
            .await?;
        Ok(controls_json(
            session_id,
            &controls_snapshot,
            &settings,
            &context,
            Some(context_limit),
            self.state.auto_compact_threshold,
        ))
    }

    async fn controls_context_usage(
        &self,
        session_id: &str,
        session: &AgentSession,
        context_limit: u32,
    ) -> BootResult<CodeWebContextUsage> {
        let stored = crate::compact::ContextJsonStore::for_session(
            code_web_store_dir(session.workspace()),
            session_id,
        )
        .load()
        .map_err(|error| BootError::Internal(error.to_string()))?;
        let compact_summary = self
            .session_context_snapshot(session_id)
            .await
            .compact_summary;

        Ok(CodeWebContextUsage {
            estimated_tokens: stored
                .as_ref()
                .map(|context| context.last_prompt_tokens)
                .unwrap_or_default(),
            limit_tokens: context_limit,
            history_messages: stored
                .as_ref()
                .map(|context| context.source_message_count)
                .unwrap_or_default(),
            compacted: compact_summary.is_some()
                || stored
                    .as_ref()
                    .is_some_and(|context| context.compact_generation > 0),
            compact_summary,
        })
    }
}