monothink 0.1.1

A totally non-agentic coding assistant
/*
 * This file is part of monothink.
 *
 * Wavething is free software: you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * Wavething is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with Wavething.
 * If not, see <https://www.gnu.org/licenses/>.
 */

use std::{fs, io::Write};

use directories::ProjectDirs;
use rig::completion::Usage;
use tokio::sync::mpsc::{Receiver, Sender};

use crate::{
    core::conversation::{Conversation, NotificationType},
    llm::LlmMessage,
    ui::UiMessage,
};

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TokenUsage {
    pub input: Option<u64>,
    pub output: Option<u64>,
}

#[derive(Clone, Debug, PartialEq)]
pub enum SessionMessage {
    SubmitPrompt(String),
    AgentResponse(String),
    AgentReasoning(String),
    AgentCompletion(Usage), // Usage is a rig type we we should abstract from. But at this point we need the same information anyhow.
    ToolCall {
        name: String,
        args: serde_json::Value,
    },
    ToolResult {
        output: String,
        success: bool,
    },
    //CancelGeneration,
    Error(String),
    Warning(String),
    QuitRequest,
}

#[derive(Debug)]
pub struct Session {
    ui_tx: Sender<UiMessage>,
    llm_tx: Sender<LlmMessage>,
    rx: Receiver<SessionMessage>,
    conversation: Conversation,
    token_usage: TokenUsage,
}

impl Session {
    pub fn new(
        ui_tx: Sender<UiMessage>,
        llm_tx: Sender<LlmMessage>,
        rx: Receiver<SessionMessage>,
    ) -> Self {
        Self {
            ui_tx,
            llm_tx,
            rx,
            conversation: Conversation::new(),
            token_usage: TokenUsage::default(),
        }
    }

    pub async fn run(&mut self) {
        loop {
            let event = self.rx.recv().await;
            if let Some(msg) = event {
                match msg {
                    SessionMessage::SubmitPrompt(prompt) => {
                        let item = self.conversation.add_user_prompt(&prompt);
                        self.llm_tx
                            .send(LlmMessage::SubmitPrompt {
                                prompt,
                                history: Vec::new(), // @todo send recent history
                            })
                            .await
                            .unwrap();
                        self.ui_tx
                            .send(UiMessage::NewConversationItem(item.to_owned()))
                            .await
                            .unwrap();
                    }
                    SessionMessage::AgentResponse(chunk) => {
                        let (item, new) = self.conversation.add_assistant_response_chunk(&chunk);
                        if new {
                            self.ui_tx
                                .send(UiMessage::NewConversationItem(item.to_owned()))
                                .await
                                .unwrap();
                        } else {
                            self.ui_tx
                                .send(UiMessage::UpdatedConversationItem(item.to_owned()))
                                .await
                                .unwrap();
                        }
                    }
                    SessionMessage::AgentReasoning(chunk) => {
                        let (item, new) = self.conversation.add_assistant_reasoning_chunk(&chunk);
                        if new {
                            self.ui_tx
                                .send(UiMessage::NewConversationItem(item.to_owned()))
                                .await
                                .unwrap();
                        } else {
                            self.ui_tx
                                .send(UiMessage::UpdatedConversationItem(item.to_owned()))
                                .await
                                .unwrap();
                        }
                    }
                    SessionMessage::AgentCompletion(usage) => {
                        self.update_token_usage(usage);
                        self.ui_tx
                            .send(UiMessage::GenerationFinished(TokenUsage {
                                input: self.token_usage.input,
                                output: self.token_usage.output,
                            }))
                            .await
                            .unwrap();

                        // publish report as system message for analyzing provider behavior
                        // bypass conversation as this is only for development
                        let msg = format!(
                            "Usage: input tokens = {}, output tokens = {}, \
                            cached input tokens = {}, cached creation input tokens = {}, \
                            reasoning tokens = {}, tool use prompt tokens = {}, total tokens = {}",
                            usage.input_tokens,
                            usage.output_tokens,
                            usage.cached_input_tokens,
                            usage.cache_creation_input_tokens,
                            usage.reasoning_tokens,
                            usage.tool_use_prompt_tokens,
                            usage.total_tokens
                        );
                        let item = self
                            .conversation
                            .add_notification(NotificationType::Info, msg);
                        self.ui_tx
                            .send(UiMessage::NewConversationItem(item.to_owned()))
                            .await
                            .unwrap();

                        // We could use this event to mark our messages as complete.
                        // But as of now a message is complete when a new message arrives.
                    }
                    SessionMessage::QuitRequest => {
                        // @todo send quit to Ui. Ui should not quit itself but send a user request
                        // to session. Session decided if app should quit or not.
                        self.llm_tx.send(LlmMessage::Quit).await.unwrap();

                        // @todo do not quit if changes are unsafed and command is not force exit
                        if !self.conversation.messages().is_empty() {
                            // save session
                            if let Some(proj_dirs) =
                                ProjectDirs::from("org", "codemonk", "monothink")
                            {
                                let mut state_dir = proj_dirs
                                    .state_dir()
                                    .expect("Running on other OS than Linux is not supported.")
                                    .to_path_buf();
                                state_dir.push("sessions");
                                state_dir.push(format!("{}.json", self.conversation.id()));
                                let session_path = state_dir.as_path();
                                // @todo stream to a writer
                                let output = serde_json::to_string(&self.conversation)
                                    .expect("Should be able to serealize a session.");
                                let mut session_file = fs::File::create(session_path)
                                    .expect("Cannot open session file for writing");
                                let _ = session_file
                                    .write(output.as_bytes())
                                    .expect("Cannot write session to file");
                            } else {
                                panic!("Could not read test data from test.json.");
                            };
                        }
                        break;
                    }
                    SessionMessage::Error(msg) => {
                        let item = self
                            .conversation
                            .add_notification(NotificationType::Error, msg);
                        self.ui_tx
                            .send(UiMessage::NewConversationItem(item.to_owned()))
                            .await
                            .unwrap();
                    }
                    SessionMessage::Warning(msg) => {
                        let item = self
                            .conversation
                            .add_notification(NotificationType::Warn, msg);
                        self.ui_tx
                            .send(UiMessage::NewConversationItem(item.to_owned()))
                            .await
                            .unwrap();
                    }
                    SessionMessage::ToolCall { name, args } => {
                        let item = self.conversation.add_tool_invocation(name, args);
                        self.ui_tx
                            .send(UiMessage::NewConversationItem(item.to_owned()))
                            .await
                            .unwrap();
                    }
                    SessionMessage::ToolResult { output, success } => {
                        let item = self.conversation.add_tool_result(output, success);
                        self.ui_tx
                            .send(UiMessage::NewConversationItem(item.to_owned()))
                            .await
                            .unwrap();
                    }
                }
            }
        }
    }

    fn update_token_usage(&mut self, usage: Usage) {
        // assume that OpenAI typical token usage reports,
        // i.e. only input and output are reported
        if usage.input_tokens > 0 {
            self.token_usage.input = Some(self.token_usage.input.unwrap_or(0) + usage.input_tokens);
        }
        if usage.output_tokens > 0 {
            self.token_usage.output =
                Some(self.token_usage.output.unwrap_or(0) + usage.output_tokens);
        }
    }
}