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
13 changes: 13 additions & 0 deletions src/apps/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,24 @@ fn terminal_scripts_dir() -> std::path::PathBuf {
}

async fn initialize_terminal_service() {
use bitfun_core::infrastructure::try_get_path_manager_arc;
use bitfun_core::service::runtime::RuntimeManager;
use bitfun_core::service::terminal::{TerminalApi, TerminalConfig};

let mut terminal_config = TerminalConfig::default();
terminal_config.shell_integration.scripts_dir = Some(terminal_scripts_dir());
match try_get_path_manager_arc() {
Ok(path_manager) => {
terminal_config.transcript.root_dir =
Some(path_manager.user_data_dir().join("terminals"));
}
Err(error) => {
tracing::warn!(
"Failed to configure terminal transcript storage; recording is disabled: {}",
error
);
}
}

if let Ok(runtime_manager) = RuntimeManager::new() {
let current_path = std::env::var("PATH").ok();
Expand Down
14 changes: 14 additions & 0 deletions src/apps/desktop/src/api/terminal_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::sync::Arc;
use tauri::{AppHandle, Emitter, State};
use tokio::sync::Mutex;

use bitfun_core::infrastructure::try_get_path_manager_arc;
use bitfun_core::service::remote_ssh::workspace_state::get_remote_workspace_manager;
use bitfun_core::service::runtime::RuntimeManager;
use bitfun_core::service::terminal::TerminalEvent;
Expand Down Expand Up @@ -47,6 +48,19 @@ impl TerminalState {
let scripts_dir = Self::get_scripts_dir();
config.shell_integration.scripts_dir = Some(scripts_dir);

match try_get_path_manager_arc() {
Ok(path_manager) => {
config.transcript.root_dir =
Some(path_manager.user_data_dir().join("terminals"));
}
Err(error) => {
warn!(
"Failed to configure terminal transcript storage; recording is disabled: {}",
error
);
}
}

// Prepend BitFun-managed runtime dirs to PATH so Bash/Skill commands can
// run on machines without preinstalled dev tools.
if let Ok(runtime_manager) = RuntimeManager::new() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::agentic::tools::implementations::ExecCommandTool;
use crate::agentic::util::remote_workspace_layout::build_remote_workspace_layout_preview;
use crate::agentic::workspace::WorkspaceBackend;
use crate::agentic::WorkspaceBinding;
use crate::infrastructure::try_get_path_manager_arc;
use crate::service::bootstrap::build_workspace_persona_prompt;
use crate::service::config::global::GlobalConfigManager;
use crate::service::config::{get_app_language_code, get_global_config_service};
Expand Down Expand Up @@ -33,6 +34,7 @@ const PLACEHOLDER_VISUAL_MODE: &str = "{VISUAL_MODE}";
const PLACEHOLDER_SESSION_ID: &str = "{SESSION_ID}";
const PLACEHOLDER_DEEP_RESEARCH_REPORT_LINK: &str = "{DEEP_RESEARCH_REPORT_LINK}";
const PLACEHOLDER_MEMORY_ROOT: &str = "{MEMORY_ROOT}";
const PLACEHOLDER_READ_TERMINAL: &str = "{READ_TERMINAL}";

#[derive(Debug, Clone)]
pub struct PromptBuilderContext {
Expand Down Expand Up @@ -414,6 +416,42 @@ Output Mermaid in fenced code blocks (```mermaid) so the UI can render them.
}
}

fn build_terminal_transcript_prompt_guidance(&self) -> String {
if self.context.remote_execution.is_some() {
return String::new();
}

match try_get_path_manager_arc() {
Ok(path_manager) => {
let agents_path = path_manager
.user_data_dir()
.join("terminals")
.join("AGENTS.md");
format!(
"## User terminal history

The user's terminal history may contain execution evidence that is missing from the conversation. Consult it when that evidence could materially affect the task, especially when:

- The user refers to a command, terminal operation, or result they previously ran or observed.
- The user reports a command-line, build, test, script, or process problem without providing the exact command or enough output to diagnose it.

Use the terminal history to recover relevant evidence before guessing or asking the user to repeat information that may already be recorded. Do not inspect it routinely when the request is unrelated to terminal activity or the conversation already contains sufficient command and output context.

For instructions on locating and reading the transcripts, read: `{}`
",
agents_path.to_string_lossy().replace('\\', "/"),
)
}
Err(error) => {
warn!(
"Failed to build terminal transcript prompt guidance; omitting it: {}",
error
);
String::new()
}
}
}

/// Get user language preference instruction
///
/// Read app.language from global config, generate simple language instruction
Expand Down Expand Up @@ -454,6 +492,7 @@ Do not read from, modify, create, move, or delete files outside this workspace u
/// - `{CLAW_WORKSPACE}` - Claw-specific workspace ownership and boundary rules
/// - `{VISUAL_MODE}` - Visual mode instruction (Mermaid diagrams, read from global config)
/// - `{MEMORY_ROOT}` - BitFun memory workspace root, used by internal memory agents
/// - `{READ_TERMINAL}` - Local user terminal transcript guidance
///
/// If a placeholder is not in the template, corresponding content will not be added
pub async fn build_prompt_from_template(&self, template: &str) -> BitFunResult<String> {
Expand Down Expand Up @@ -499,6 +538,11 @@ Do not read from, modify, create, move, or delete files outside this workspace u
result = result.replace(PLACEHOLDER_VISUAL_MODE, &visual_mode);
}

if result.contains(PLACEHOLDER_READ_TERMINAL) {
let read_terminal = self.build_terminal_transcript_prompt_guidance();
result = result.replace(PLACEHOLDER_READ_TERMINAL, &read_terminal);
}

// Replace {SESSION_ID} — used by deep-research Pro mode to anchor a per-session
// work_dir under .bitfun/sessions/{SESSION_ID}/research/. Falls back to a
// timestamp slug when no session is bound (e.g. one-shot prompt builds in tests).
Expand Down Expand Up @@ -803,6 +847,62 @@ mod tests {
assert!(!runtime_context.contains("Local BitFun client OS:"));
}

#[tokio::test]
async fn local_terminal_transcript_placeholder_includes_the_agents_path() {
let context = PromptBuilderContext::new("workspace/root", None, None);
let prompt = PromptBuilder::new(context)
.build_prompt_from_template("{READ_TERMINAL}")
.await
.expect("prompt should build");
let expected_path = crate::infrastructure::try_get_path_manager_arc()
.expect("path manager should initialize")
.user_data_dir()
.join("terminals")
.join("AGENTS.md")
.to_string_lossy()
.replace('\\', "/");

assert!(prompt.contains(&expected_path));
assert!(prompt.contains(
"The user refers to a command, terminal operation, or result they previously ran or observed."
));
assert!(prompt
.contains("The user reports a command-line, build, test, script, or process problem"));
assert!(prompt.contains("Do not inspect it routinely"));
}

#[tokio::test]
async fn remote_terminal_transcript_placeholder_is_omitted() {
let context = PromptBuilderContext::new("/workspace/project", None, None)
.with_remote_prompt_overlay(
RemoteExecutionHints {
connection_display_name: "dev-server".to_string(),
kernel_name: "Linux".to_string(),
hostname: "devbox".to_string(),
},
None,
);
let prompt = PromptBuilder::new(context)
.build_prompt_from_template("before\n{READ_TERMINAL}\nafter")
.await
.expect("prompt should build");

assert!(!prompt.contains("User terminal transcript"));
assert!(!prompt.contains("{READ_TERMINAL}"));
assert_eq!(prompt, "before\n\nafter");
}

#[tokio::test]
async fn template_without_terminal_transcript_placeholder_is_unchanged() {
let context = PromptBuilderContext::new("workspace/root", None, None);
let prompt = PromptBuilder::new(context)
.build_prompt_from_template("plain template")
.await
.expect("prompt should build");

assert_eq!(prompt, "plain template");
}

#[tokio::test]
async fn deep_research_report_link_defaults_to_workspace_relative_path() {
let context =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ When presenting options, state your recommendation and reasoning, keep choices c
When presenting options or plans, never include time estimates - focus on what each option involves, not how long it might take.

{VISUAL_MODE}

# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
- Read relevant code before proposing concrete changes to it. For broad design discussion, state assumptions and inspect files before editing.
Expand Down Expand Up @@ -114,3 +115,4 @@ IMPORTANT: Whenever you mention a file path that the user might want to open, ma
</bad-examples>

{LANGUAGE_PREFERENCE}
{READ_TERMINAL}
1 change: 1 addition & 0 deletions src/crates/services/terminal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dashmap = { workspace = true }
dirs = { workspace = true }

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

[target.'cfg(windows)'.dependencies]
# Windows process-tree ownership
Expand Down
35 changes: 35 additions & 0 deletions src/crates/services/terminal/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub struct TerminalConfig {
/// Terminal dimensions
pub default_cols: u16,
pub default_rows: u16,

/// Persistent plain-text transcript settings for user-created terminal sessions.
#[serde(default)]
pub transcript: TerminalTranscriptConfig,
}

impl Default for TerminalConfig {
Expand All @@ -49,6 +53,37 @@ impl Default for TerminalConfig {
shell_integration: ShellIntegrationConfig::default(),
default_cols: 80,
default_rows: 24,
transcript: TerminalTranscriptConfig::default(),
}
}
}

/// Persistent plain-text transcript settings for user-created terminal sessions.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TerminalTranscriptConfig {
/// Transcript root directory. `None` disables recording so reusable terminal-core
/// consumers do not choose a product storage location themselves.
#[serde(default)]
pub root_dir: Option<PathBuf>,

/// Maximum size of a single append-only segment before the next write rotates it.
pub segment_size_bytes: u64,

/// Number of recent segments retained for one terminal session.
pub retained_segments_per_session: usize,

/// Number of recent sessions retained unless more sessions are currently active.
pub max_recent_sessions: usize,
}

impl Default for TerminalTranscriptConfig {
fn default() -> Self {
Self {
root_dir: None,
segment_size_bytes: 4 * 1024 * 1024,
retained_segments_per_session: 4,
max_recent_sessions: 10,
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/crates/services/terminal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ pub mod pty;
pub mod runtime_port;
pub mod session;
pub mod shell;
mod transcript;

// Re-export main types for convenience
pub use api::{
AcknowledgeRequest, CloseSessionRequest, CreateSessionRequest, ExecuteCommandRequest,
ExecuteCommandResponse, GetHistoryRequest, GetHistoryResponse, ResizeRequest,
SendCommandRequest, SessionResponse, ShellInfo, SignalRequest, TerminalApi, WriteRequest,
};
pub use config::{ShellConfig, TerminalConfig};
pub use config::{ShellConfig, TerminalConfig, TerminalTranscriptConfig};
pub use events::{TerminalEvent, TerminalEventEmitter};
pub use exec::{
get_global_exec_process_manager, ExecCommandRequest as LocalExecCommandRequest,
Expand Down
Loading
Loading