Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions crates/chat/src/service/chat_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,9 +1011,9 @@ impl ChatService for LiveChatService {
runtime_context.mode = resolve_prompt_mode_context(&persona.config, session_entry.as_ref());
apply_request_runtime_context(&mut runtime_context.host, &params);

// Resolve project context.
// Resolve project context plus optional command-generated context.
let project_context = self
.resolve_project_context(&session_key, conn_id.as_deref())
.resolve_turn_context(&session_key, conn_id.as_deref())
.await;

// Discover skills (gated on `[skills] enabled` — see #655).
Expand Down Expand Up @@ -1151,9 +1151,9 @@ impl ChatService for LiveChatService {
runtime_context.mode = resolve_prompt_mode_context(&persona.config, session_entry.as_ref());
apply_request_runtime_context(&mut runtime_context.host, &params);

// Resolve project context.
// Resolve project context plus optional command-generated context.
let project_context = self
.resolve_project_context(&session_key, conn_id.as_deref())
.resolve_turn_context(&session_key, conn_id.as_deref())
.await;

// Discover skills (gated on `[skills] enabled` — see #655).
Expand Down
4 changes: 2 additions & 2 deletions crates/chat/src/service/chat_impl/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,9 +605,9 @@ impl LiveChatService {
}
}

// Resolve project context for this connection's active project.
// Resolve project context plus optional command-generated context.
let project_context = self
.resolve_project_context(&session_key, conn_id.as_deref())
.resolve_turn_context(&session_key, conn_id.as_deref())
.await;

// Generate run_id early so we can link the user message to its agent run.
Expand Down
87 changes: 76 additions & 11 deletions crates/chat/src/service/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::{
collections::{HashMap, HashSet},
path::Path,
path::{Path, PathBuf},
sync::Arc,
};

Expand Down Expand Up @@ -537,12 +537,17 @@ impl LiveChatService {
self.session_key_for(conn_id).await
}

/// Resolve the project context prompt section for a session.
/// Resolve the project context prompt section and effective working
/// directory for a session.
///
/// The working directory is the session worktree when present, otherwise
/// the bound project directory; it is `None` when no project is bound. It
/// is used to run the configured `context_command` in the expected place.
pub(in crate::service) async fn resolve_project_context(
&self,
session_key: &str,
conn_id: Option<&str>,
) -> Option<String> {
) -> (Option<String>, Option<PathBuf>) {
let project_id = if let Some(cid) = conn_id {
self.state.active_project_id(cid).await
} else {
Expand All @@ -558,22 +563,30 @@ impl LiveChatService {
.and_then(|e| e.project_id),
};

let pid = project_id?;
let val = self
let Some(pid) = project_id else {
return (None, None);
};
let Ok(val) = self
.state
.project_service()
.get(serde_json::json!({"id": pid}))
.await
.ok()?;
let dir = val.get("directory").and_then(|v| v.as_str())?;
else {
return (None, None);
};
let Some(dir) = val.get("directory").and_then(|v| v.as_str()) else {
return (None, None);
};
let files = match moltis_projects::context::load_context_files(Path::new(dir)) {
Ok(f) => f,
Err(e) => {
warn!("failed to load project context: {e}");
return None;
return (None, None);
},
};
let project: moltis_projects::Project = serde_json::from_value(val.clone()).ok()?;
let Ok(project) = serde_json::from_value::<moltis_projects::Project>(val.clone()) else {
return (None, None);
};
let worktree_dir = self
.session_metadata
.get(session_key)
Expand All @@ -587,21 +600,53 @@ impl LiveChatService {
None
}
});
// The command runs in the session worktree when present, else the
// project root — matching where the operator's scripts expect to be.
let working_dir = worktree_dir.clone().unwrap_or_else(|| PathBuf::from(dir));
let ctx = moltis_projects::ProjectContext {
project,
context_files: files,
worktree_dir,
};
Some(ctx.to_prompt_section())
(Some(ctx.to_prompt_section()), Some(working_dir))
}

/// Resolve all dynamic prompt context for a turn.
pub(in crate::service) async fn resolve_turn_context(
&self,
session_key: &str,
conn_id: Option<&str>,
) -> Option<String> {
let (project_context, working_dir) =
self.resolve_project_context(session_key, conn_id).await;
let command_context = moltis_common::context_command::run_context_command(
self.config.chat.context_command.as_deref(),
working_dir.as_deref(),
)
.await;
merge_context_sections(project_context, command_context)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

pub(in crate::service) fn merge_context_sections(
project_context: Option<String>,
command_context: Option<String>,
) -> Option<String> {
match (project_context, command_context) {
(Some(project), Some(command)) => Some(format!("{project}\n\n{command}")),
(Some(project), None) => Some(project),
(None, Some(command)) => Some(command),
(None, None) => None,
}
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use {
super::{
ActiveAssistantDraft, build_persisted_assistant_message,
build_tool_call_assistant_message,
build_tool_call_assistant_message, merge_context_sections,
},
crate::types::AssistantTurnOutput,
moltis_sessions::PersistedMessage,
Expand Down Expand Up @@ -636,6 +681,26 @@ mod tests {
}
}

#[test]
fn merge_context_sections_combines_project_and_command_context() {
let merged = merge_context_sections(Some("project".into()), Some("dynamic".into()))
.expect("merged context");
assert_eq!(merged, "project\n\ndynamic");
}

#[test]
fn merge_context_sections_keeps_single_context() {
assert_eq!(
merge_context_sections(Some("project".into()), None).as_deref(),
Some("project")
);
assert_eq!(
merge_context_sections(None, Some("dynamic".into())).as_deref(),
Some("dynamic")
);
assert_eq!(merge_context_sections(None, None), None);
}

#[test]
fn tool_call_assistant_message_omits_cache_usage_fields() {
let message = build_tool_call_assistant_message(
Expand Down
3 changes: 3 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ tracing = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }

[features]
default = []
metrics = ["dep:moltis-metrics"]
Expand Down
Loading