pub mod conversation_buffer;
pub mod detection;
pub mod knowledge_bridge;
pub mod manager;
pub use conversation_buffer::{ConversationBuffer, ConversationTurn};
pub use detection::{extract_filesystem_path, match_keywords, PathMatcher};
pub use knowledge_bridge::{CrossRefEntry, KnowledgeBridge, KnowledgeFlow};
pub use manager::{SpaceManager, SpaceManagerError};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub type SpaceId = Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpaceSource {
AutoResource,
AutoTopic,
Manual,
}
impl std::fmt::Display for SpaceSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpaceSource::AutoResource => write!(f, "auto_resource"),
SpaceSource::AutoTopic => write!(f, "auto_topic"),
SpaceSource::Manual => write!(f, "manual"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Space {
pub id: SpaceId,
pub name: String,
pub source: SpaceSource,
pub paths: Vec<PathBuf>,
pub workspace_dir: PathBuf,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub active: bool,
pub created_at: DateTime<Utc>,
pub last_active_at: DateTime<Utc>,
#[serde(default)]
pub interaction_count: u64,
#[serde(default = "default_true")]
pub knowledge_visible: bool,
}
fn default_true() -> bool {
true
}
impl Space {
pub fn new(name: impl Into<String>, source: SpaceSource) -> Self {
let now = Utc::now();
Self {
id: SpaceId::new_v4(),
name: name.into(),
source,
paths: Vec::new(),
workspace_dir: PathBuf::new(),
tags: Vec::new(),
active: false,
created_at: now,
last_active_at: now,
interaction_count: 0,
knowledge_visible: true,
}
}
pub fn from_path(path: &Path) -> Self {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let mut space = Self::new(&name, SpaceSource::AutoResource);
space.paths.push(path.to_path_buf());
space
}
pub fn from_topic(topic: &str) -> Self {
Self::new(topic, SpaceSource::AutoTopic)
}
pub fn touch(&mut self) {
self.last_active_at = Utc::now();
self.interaction_count += 1;
}
pub fn activate(&mut self) {
self.active = true;
}
pub fn deactivate(&mut self) {
self.active = false;
}
pub fn is_named(&self) -> bool {
!self.name.is_empty()
}
pub fn is_default(&self) -> bool {
self.name.is_empty()
}
pub fn emoji(&self) -> &'static str {
if self.name.is_empty() {
"⚪"
} else {
match self.name.to_lowercase().as_str() {
"oxios" | "dev" | "개발" => "🔧",
"일상" | "daily" | "生活" => "🏠",
"blog" | "블로그" => "📝",
"docs" | "문서" => "📄",
"study" | "공부" | "학습" => "📚",
"cook" | "요리" | "recipe" | "레시피" => "🍳",
"work" | "업무" => "💼",
_ => "📦",
}
}
}
pub fn add_tag(&mut self, tag: impl Into<String>) {
let tag = tag.into();
if !self.tags.contains(&tag) {
self.tags.push(tag);
}
}
}
#[allow(missing_docs)]
pub static DEFAULT_SPACE_ID: std::sync::OnceLock<uuid::Uuid> = std::sync::OnceLock::new();
pub fn default_space_id() -> SpaceId {
*DEFAULT_SPACE_ID
.get_or_init(|| uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_space_new() {
let s = Space::new("oxios", SpaceSource::AutoResource);
assert_eq!(s.name, "oxios");
assert_eq!(s.source, SpaceSource::AutoResource);
assert!(!s.active);
assert_eq!(s.interaction_count, 0);
}
#[test]
fn test_space_from_path() {
let path = PathBuf::from("/projects/oxios");
let s = Space::from_path(&path);
assert_eq!(s.name, "oxios");
assert_eq!(s.source, SpaceSource::AutoResource);
assert_eq!(s.paths, vec![path]);
}
#[test]
fn test_space_touch() {
let mut s = Space::new("test", SpaceSource::Manual);
assert_eq!(s.interaction_count, 0);
s.touch();
assert_eq!(s.interaction_count, 1);
}
#[test]
fn test_space_emoji() {
let mut s = Space::new("", SpaceSource::Manual);
assert_eq!(s.emoji(), "⚪");
let mut s = Space::new("oxios", SpaceSource::AutoResource);
assert_eq!(s.emoji(), "🔧");
let mut s = Space::new("일상", SpaceSource::AutoTopic);
assert_eq!(s.emoji(), "🏠");
let mut s = Space::new("random", SpaceSource::Manual);
assert_eq!(s.emoji(), "📦");
}
#[test]
fn test_space_default() {
let s = Space::new("", SpaceSource::Manual);
assert!(s.is_default());
assert!(!s.is_named());
let s = Space::new("oxios", SpaceSource::AutoResource);
assert!(!s.is_default());
assert!(s.is_named());
}
}