Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Chats: start a session without a project from **Chats** in the project rail or **File → New Chat** (`⌘N` / `Ctrl+N`). Chats are saved and listed in their own sidebar section, where they can be renamed, pinned, archived, searched and deleted like project sessions, and they run in a folder MonoCode keeps for them instead of the home directory. A chat can be moved into a project later from the project picker in its composer: the conversation is carried over as a brief on the next turn, and files the agent wrote in the chats folder stay there. In #757.

## [0.8.0] - 2026-10-06

### Added
Expand Down
14 changes: 14 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ fn home_dir() -> String {
.unwrap_or_else(|| "~".into())
}

/// Folder projectless chats run in, so an agent with no project is not turned
/// loose on the whole home directory.
#[tauri::command]
fn chat_workspace_dir(app: tauri::AppHandle) -> Result<String, String> {
let dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?
.join("chats");
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
Ok(fs::path_to_js(&dir))
}

pub(crate) struct PasswdIdentity {
pub home: String,
pub user: String,
Expand Down Expand Up @@ -282,6 +295,7 @@ pub fn run() {
control::app_cli_path,
default_cwd,
home_dir,
chat_workspace_dir,
notifications::notification_permission,
notifications::request_notification_permission,
notifications::show_notification,
Expand Down
16 changes: 13 additions & 3 deletions src-tauri/src/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,17 @@ pub fn dispatch(app: &AppHandle, id: &str) {
| "open_settings" | "check_for_updates" => {
let _ = app.emit(id, ());
}
// Sidebar, Zoom, Reload, Command Palette, and Close All Tabs target one window: a broadcast would
// make every window act on a single menu click.
// Sidebar, Zoom, Reload, Command Palette, Close All Tabs, and New Chat target one window: a
// broadcast would make every window act on a single menu click.
"toggle_sidebar"
| "toggle_session_sidebar"
| "zoom_in"
| "zoom_out"
| "zoom_reset"
| "reload"
| "open_command_palette"
| "close_all_tabs" => emit_to_focused(app, id),
| "close_all_tabs"
| "new_chat" => emit_to_focused(app, id),
_ => {}
}
}
Expand Down Expand Up @@ -254,6 +255,14 @@ fn build(
"Tab: New",
overrides,
)?;
let new_chat = menu_item(
app,
"new_chat",
"New Chat",
"CmdOrCtrl+N",
"Chat: New",
overrides,
)?;
let new_terminal = menu_item(
app,
"new_terminal",
Expand Down Expand Up @@ -461,6 +470,7 @@ fn build(
.item(&autosave)
.separator()
.item(&new_tab)
.item(&new_chat)
.item(&new_terminal)
.item(&new_terminal_tab)
.item(&split_right)
Expand Down
34 changes: 31 additions & 3 deletions src-tauri/src/session_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,13 +1145,13 @@ pub(crate) fn upsert_session(
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty());
let git = crate::fs::git_info_for(&crate::fs::expand_home(
let git = git_info_for_cwd(
session
.worktree_cwd
.as_deref()
.filter(|cwd| !cwd.is_empty())
.unwrap_or(&session.cwd),
));
);
let branch = if session.worktree_removed {
None
} else {
Expand Down Expand Up @@ -1627,8 +1627,17 @@ fn ceil_char_boundary(text: &str, mut index: usize) -> usize {
index
}

/// Projectless chats are stored under `~`. They have no repository, and asking
/// git about the home directory would only walk it for nothing.
fn git_info_for_cwd(cwd: &str) -> crate::fs::GitInfo {
if cwd == "~" {
return crate::fs::GitInfo::default();
}
crate::fs::git_info_for(&crate::fs::expand_home(cwd))
}

fn list_by_project(conn: &Connection, cwd: &str) -> rusqlite::Result<Vec<SessionSummary>> {
let git = crate::fs::git_info_for(&crate::fs::expand_home(cwd));
let git = git_info_for_cwd(cwd);
let mut statement = conn.prepare(
"SELECT id, cwd, harness, model, runtime_mode, title, provider_session_id,
created_at, updated_at, branch, archived, pinned,
Expand Down Expand Up @@ -2688,6 +2697,25 @@ mod tests {
assert_eq!(listed[1].id, "s1");
}

#[test]
fn list_by_project_lists_projectless_chats() {
let store = SessionStore::open_in_memory().unwrap();
let conn = store.conn.lock().unwrap();
upsert_session(&conn, &sample("chat", "~", "Chat")).unwrap();
upsert_session(&conn, &sample("s1", "/tmp/a", "A1")).unwrap();
let chats = list_by_project(&conn, "~").unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(chats[0].id, "chat");
assert_eq!(chats[0].cwd, "~");
assert!(chats[0].branch.is_none());
assert!(chats[0].repo.is_none());

// Moving the chat into a project takes it out of the chats list.
upsert_session(&conn, &sample("chat", "/tmp/a", "Chat")).unwrap();
assert!(list_by_project(&conn, "~").unwrap().is_empty());
assert_eq!(list_by_project(&conn, "/tmp/a").unwrap().len(), 2);
}

#[test]
fn delete_removes_session() {
let store = SessionStore::open_in_memory().unwrap();
Expand Down
Loading