Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ccb64f2
fix: accept alt collaboration modes responses and refine debug logging
ishanray Feb 4, 2026
de1696f
feat: include experimentalApi capability in initialize params
ishanray Feb 4, 2026
0c72298
Show collaboration modes verbatim
ishanray Feb 4, 2026
e93e790
refactor: remove unused collaboration mode label formatter
ishanray Feb 4, 2026
d3b0fe9
feat(notifications): add response-required system notifications
ishanray Feb 4, 2026
9d98a3a
Fix plan panel visibility
ishanray Feb 4, 2026
57a97fd
feat(composer): enable shortcuts on workspace home textarea
ishanray Feb 5, 2026
1ee5879
feat(worktrees): add branch suggestions dropdown to WorktreePrompt
ishanray Feb 5, 2026
3dc1b55
Merge branch 'worktree-agent-modal'
ishanray Feb 5, 2026
ba5622c
fix(messages): keep streamed plan output when completion is empty
ishanray Feb 5, 2026
be336ea
feat(git): add per-file discard action in local diff viewer
ishanray Feb 5, 2026
b17b97a
chore: fix indentation
ishanray Feb 5, 2026
70a7c68
fix(messages): keep streamed plan output when completion is empty
ishanray Feb 5, 2026
ecb718b
feat(git): add per-file discard action in local diff viewer
ishanray Feb 5, 2026
c56c6dd
Merge branch 'testing-ideas'
ishanray Feb 5, 2026
d7b1f23
Merge branch 'diff-revert'
ishanray Feb 5, 2026
0f4eea6
feat(workspaces): copy AGENTS.md into new worktrees
ishanray Feb 5, 2026
ba763a7
feat(settings): add Environments section for per-project setup scripts
ishanray Feb 5, 2026
7d7f7b3
feat(workspaces): allow skipping AGENTS.md copy when creating worktrees
ishanray Feb 5, 2026
18b8baf
fix(workspaces): avoid overwriting worktree AGENTS.md
Dimillian Feb 5, 2026
f19dd1b
fix: prefer non-empty tool output over longer streamed output
Dimillian Feb 5, 2026
20f2456
fix: retry throttled response-required notifications and queue plans
Dimillian Feb 5, 2026
2ee96d1
fix(notifications): notify all pending approvals and questions
Dimillian Feb 5, 2026
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
35 changes: 27 additions & 8 deletions src-tauri/src/backend/app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ fn extract_thread_id(value: &Value) -> Option<String> {
})
}

fn build_initialize_params(client_version: &str) -> Value {
json!({
"clientInfo": {
"name": "codex_monitor",
"title": "Codex Monitor",
"version": client_version
},
"capabilities": {
"experimentalApi": true
}
})
}

pub(crate) struct WorkspaceSession {
pub(crate) entry: WorkspaceEntry,
pub(crate) child: Mutex<Child>,
Expand Down Expand Up @@ -332,13 +345,7 @@ pub(crate) async fn spawn_workspace_session<E: EventSink>(
}
});

let init_params = json!({
"clientInfo": {
"name": "codex_monitor",
"title": "Codex Monitor",
"version": client_version
}
});
let init_params = build_initialize_params(&client_version);
let init_result = timeout(
Duration::from_secs(15),
session.send_request("initialize", init_params),
Expand Down Expand Up @@ -372,7 +379,7 @@ pub(crate) async fn spawn_workspace_session<E: EventSink>(

#[cfg(test)]
mod tests {
use super::extract_thread_id;
use super::{build_initialize_params, extract_thread_id};
use serde_json::json;

#[test]
Expand All @@ -392,4 +399,16 @@ mod tests {
let value = json!({ "params": {} });
assert_eq!(extract_thread_id(&value), None);
}

#[test]
fn build_initialize_params_enables_experimental_api() {
let params = build_initialize_params("1.2.3");
assert_eq!(
params
.get("capabilities")
.and_then(|caps| caps.get("experimentalApi"))
.and_then(|value| value.as_bool()),
Some(true)
);
}
}
12 changes: 11 additions & 1 deletion src-tauri/src/bin/codex_monitor_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,15 @@ impl DaemonState {
parent_id: String,
branch: String,
name: Option<String>,
copy_agents_md: bool,
client_version: String,
) -> Result<WorkspaceInfo, String> {
let client_version = client_version.clone();
workspaces_core::add_worktree_core(
parent_id,
branch,
name,
copy_agents_md,
&self.data_dir,
&self.workspaces,
&self.sessions,
Expand Down Expand Up @@ -914,6 +916,13 @@ fn parse_optional_u32(value: &Value, key: &str) -> Option<u32> {
}
}

fn parse_optional_bool(value: &Value, key: &str) -> Option<bool> {
match value {
Value::Object(map) => map.get(key).and_then(|value| value.as_bool()),
_ => None,
}
}

fn parse_optional_string_array(value: &Value, key: &str) -> Option<Vec<String>> {
match value {
Value::Object(map) => map.get(key).and_then(|value| value.as_array()).map(|items| {
Expand Down Expand Up @@ -989,8 +998,9 @@ async fn handle_rpc_request(
let parent_id = parse_string(&params, "parentId")?;
let branch = parse_string(&params, "branch")?;
let name = parse_optional_string(&params, "name");
let copy_agents_md = parse_optional_bool(&params, "copyAgentsMd").unwrap_or(true);
let workspace = state
.add_worktree(parent_id, branch, name, client_version)
.add_worktree(parent_id, branch, name, copy_agents_md, client_version)
.await?;
serde_json::to_value(workspace).map_err(|err| err.to_string())
}
Expand Down
103 changes: 103 additions & 0 deletions src-tauri/src/shared/workspaces_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,44 @@ use uuid::Uuid;

pub(crate) const WORKTREE_SETUP_MARKERS_DIR: &str = "worktree-setup";
pub(crate) const WORKTREE_SETUP_MARKER_EXT: &str = "ran";
const AGENTS_MD_FILE_NAME: &str = "AGENTS.md";

fn copy_agents_md_from_parent_to_worktree(
parent_repo_root: &PathBuf,
worktree_root: &PathBuf,
) -> Result<(), String> {
let source_path = parent_repo_root.join(AGENTS_MD_FILE_NAME);
if !source_path.is_file() {
return Ok(());
}

let destination_path = worktree_root.join(AGENTS_MD_FILE_NAME);
if destination_path.is_file() {
return Ok(());
}

let temp_path = worktree_root.join(format!("{AGENTS_MD_FILE_NAME}.tmp"));

std::fs::copy(&source_path, &temp_path).map_err(|err| {
format!(
"Failed to copy {} from {} to {}: {err}",
AGENTS_MD_FILE_NAME,
source_path.display(),
temp_path.display()
)
})?;

std::fs::rename(&temp_path, &destination_path).map_err(|err| {
let _ = std::fs::remove_file(&temp_path);
format!(
"Failed to finalize {} copy to {}: {err}",
AGENTS_MD_FILE_NAME,
destination_path.display()
)
})?;

Ok(())
}

pub(crate) fn normalize_setup_script(script: Option<String>) -> Option<String> {
match script {
Expand Down Expand Up @@ -248,6 +286,7 @@ pub(crate) async fn add_worktree_core<
parent_id: String,
branch: String,
name: Option<String>,
copy_agents_md: bool,
data_dir: &PathBuf,
workspaces: &Mutex<HashMap<String, WorkspaceEntry>>,
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
Expand Down Expand Up @@ -337,6 +376,17 @@ where
.await?;
}

if copy_agents_md {
if let Err(error) = copy_agents_md_from_parent_to_worktree(&repo_path, &worktree_path) {
eprintln!(
"add_worktree: optional {} copy failed for {}: {}",
AGENTS_MD_FILE_NAME,
worktree_path.display(),
error
);
}
}

let entry = WorkspaceEntry {
id: Uuid::new_v4().to_string(),
name: name.clone().unwrap_or_else(|| branch.clone()),
Expand Down Expand Up @@ -1118,3 +1168,56 @@ fn sort_workspaces(workspaces: &mut [WorkspaceInfo]) {
.then_with(|| a.id.cmp(&b.id))
});
}

#[cfg(test)]
mod tests {
use super::copy_agents_md_from_parent_to_worktree;
use super::AGENTS_MD_FILE_NAME;
use uuid::Uuid;

fn make_temp_dir() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("codex-monitor-{}", Uuid::new_v4()));
std::fs::create_dir_all(&dir).expect("failed to create temp dir");
dir
}

#[test]
fn copies_agents_md_when_missing_in_worktree() {
let parent = make_temp_dir();
let worktree = make_temp_dir();
let parent_agents = parent.join(AGENTS_MD_FILE_NAME);
let worktree_agents = worktree.join(AGENTS_MD_FILE_NAME);

std::fs::write(&parent_agents, "parent").expect("failed to write parent AGENTS.md");

copy_agents_md_from_parent_to_worktree(&parent, &worktree).expect("copy should succeed");

let copied = std::fs::read_to_string(&worktree_agents)
.expect("worktree AGENTS.md should exist after copy");
assert_eq!(copied, "parent");

let _ = std::fs::remove_dir_all(parent);
let _ = std::fs::remove_dir_all(worktree);
}

#[test]
fn does_not_overwrite_existing_worktree_agents_md() {
let parent = make_temp_dir();
let worktree = make_temp_dir();
let parent_agents = parent.join(AGENTS_MD_FILE_NAME);
let worktree_agents = worktree.join(AGENTS_MD_FILE_NAME);

std::fs::write(&parent_agents, "parent").expect("failed to write parent AGENTS.md");
std::fs::write(&worktree_agents, "branch-specific")
.expect("failed to write worktree AGENTS.md");

copy_agents_md_from_parent_to_worktree(&parent, &worktree).expect("copy should succeed");

let retained = std::fs::read_to_string(&worktree_agents)
.expect("worktree AGENTS.md should still exist");
assert_eq!(retained, "branch-specific");

let _ = std::fs::remove_dir_all(parent);
let _ = std::fs::remove_dir_all(worktree);
}
}
10 changes: 9 additions & 1 deletion src-tauri/src/workspaces/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,15 +285,22 @@ pub(crate) async fn add_worktree(
parent_id: String,
branch: String,
name: Option<String>,
copy_agents_md: Option<bool>,
state: State<'_, AppState>,
app: AppHandle,
) -> Result<WorkspaceInfo, String> {
let copy_agents_md = copy_agents_md.unwrap_or(true);
if remote_backend::is_remote_mode(&*state).await {
let response = remote_backend::call_remote(
&*state,
app,
"add_worktree",
json!({ "parentId": parent_id, "branch": branch, "name": name }),
json!({
"parentId": parent_id,
"branch": branch,
"name": name,
"copyAgentsMd": copy_agents_md
}),
)
.await?;
return serde_json::from_value(response).map_err(|err| err.to_string());
Expand All @@ -308,6 +315,7 @@ pub(crate) async fn add_worktree(
parent_id,
branch,
name,
copy_agents_md,
&data_dir,
&state.workspaces,
&state.sessions,
Expand Down
35 changes: 30 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
} from "./features/layout/components/SidebarToggleControls";
import { useAppSettingsController } from "./features/app/hooks/useAppSettingsController";
import { useUpdaterController } from "./features/app/hooks/useUpdaterController";
import { useResponseRequiredNotificationsController } from "./features/app/hooks/useResponseRequiredNotificationsController";
import { useErrorToasts } from "./features/notifications/hooks/useErrorToasts";
import { useComposerShortcuts } from "./features/composer/hooks/useComposerShortcuts";
import { useComposerMenuActions } from "./features/composer/hooks/useComposerMenuActions";
Expand Down Expand Up @@ -421,8 +422,7 @@ function MainApp() {
onDebug: addDebugEntry,
});

useComposerShortcuts({
textareaRef: composerInputRef,
const composerShortcuts = {
modelShortcut: appSettings.composerModelShortcut,
accessShortcut: appSettings.composerAccessShortcut,
reasoningShortcut: appSettings.composerReasoningShortcut,
Expand All @@ -441,6 +441,16 @@ function MainApp() {
selectedEffort,
onSelectEffort: setSelectedEffort,
reasoningSupported,
};

useComposerShortcuts({
textareaRef: composerInputRef,
...composerShortcuts,
});

useComposerShortcuts({
textareaRef: workspaceHomeTextareaRef,
...composerShortcuts,
});

useComposerMenuActions({
Expand Down Expand Up @@ -703,6 +713,15 @@ function MainApp() {
customPrompts: prompts,
onMessageActivity: queueGitStatusRefresh
});

useResponseRequiredNotificationsController({
systemNotificationsEnabled: appSettings.systemNotificationsEnabled,
approvals,
userInputRequests,
getWorkspaceName,
onDebug: addDebugEntry,
});

const {
activeAccount,
accountSwitching,
Expand Down Expand Up @@ -898,6 +917,7 @@ function MainApp() {
cancelPrompt: cancelWorktreePrompt,
updateName: updateWorktreeName,
updateBranch: updateWorktreeBranch,
updateCopyAgentsMd: updateWorktreeCopyAgentsMd,
updateSetupScript: updateWorktreeSetupScript,
} = useWorktreePrompt({
addWorktreeAgent,
Expand Down Expand Up @@ -1046,9 +1066,12 @@ function MainApp() {
const activePlan = activeThreadId
? planByThread[activeThreadId] ?? null
: null;
const hasActivePlan = Boolean(
activePlan && (activePlan.steps.length > 0 || activePlan.explanation)
);
const activeThreadProcessing = activeThreadId
? threadStatusById[activeThreadId]?.isProcessing ?? false
: false;
const hasActivePlan =
Boolean(activePlan && (activePlan.steps.length > 0 || activePlan.explanation)) ||
activeThreadProcessing;
const showHome = !activeWorkspace;
const showWorkspaceHome = Boolean(activeWorkspace && !activeThreadId && !isNewAgentDraftMode);
const showComposer = (!isCompact
Expand Down Expand Up @@ -1925,6 +1948,7 @@ function MainApp() {
selectedDiffPath,
diffScrollRequestId,
onSelectDiff: handleSelectDiff,
diffSource,
gitLogEntries,
gitLogTotal,
gitLogAhead,
Expand Down Expand Up @@ -2282,6 +2306,7 @@ function MainApp() {
worktreePrompt={worktreePrompt}
onWorktreePromptNameChange={updateWorktreeName}
onWorktreePromptChange={updateWorktreeBranch}
onWorktreePromptCopyAgentsMdChange={updateWorktreeCopyAgentsMd}
onWorktreeSetupScriptChange={updateWorktreeSetupScript}
onWorktreePromptCancel={cancelWorktreePrompt}
onWorktreePromptConfirm={confirmWorktreePrompt}
Expand Down
Loading