Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9cee179
Agent: group the sidebar under project folders
doowta Oct 23, 2026
f4bdf24
Agent: let a menu item open a submenu
doowta Oct 24, 2026
d00ac89
Agent: add sidebar View options
doowta Oct 23, 2026
771a409
Agent: show View options as submenus
doowta Oct 23, 2026
9659601
Agent: support manual sidebar folder ordering
marksftw Oct 26, 2026
e973c4e
Agent: honor folder sort choices and mark drag insertion
marksftw Oct 27, 2026
00136a4
Agent: date-sort empty projects using saved metadata
marksftw Oct 27, 2026
65a79ef
Agent: unify sidebar grouping and manual task order
marksftw Oct 27, 2026
f7e036c
Agent: fix project moves and separate removal action
marksftw Oct 27, 2026
de74df9
Agent: group task menu actions and isolate deletion
marksftw Oct 27, 2026
0be98a5
Agent: separate project ordering menu section
marksftw Oct 27, 2026
1490c7d
Agent: align remote compatibility coverage after master sync
marksftw Oct 27, 2026
6e4eba4
Agent: keep sidebar ordering local and restore remote protocol 7
marksftw Oct 27, 2026
adbcb13
Agent: choose a computer before adding a sidebar project
marksftw Oct 27, 2026
ba4105b
Agent: label the new project computer chooser
marksftw Oct 27, 2026
c1f02e4
Agent: fix remote sidebar state and startup selection
marksftw Oct 27, 2026
3d1fd80
Agent: unify drop cues and add per-computer project actions
marksftw Oct 27, 2026
b4f63da
Agent: align connection names with project labels
marksftw Oct 27, 2026
dbc8cef
Agent: render computer icons beside aligned connection names
marksftw Oct 27, 2026
a473dea
Agent: center connection chevrons in the left gutter
marksftw Oct 27, 2026
d02f623
Agent: open sidebar View menu toward the right
marksftw Oct 27, 2026
3de538c
Agent: remove superseded sidebar header and ordering code
marksftw Oct 27, 2026
1aa7175
Keep sidebar task layout stable across computers
marksftw Oct 27, 2026
915ac87
Show pinned tasks above all sidebar groups
marksftw Oct 27, 2026
3e5bb71
Use the task identity when clicking Unpin
marksftw Oct 27, 2026
beeb0f5
Keep the normal cursor over project rows
marksftw Oct 27, 2026
0a4e348
Fix sidebar review regressions and popup placement
marksftw Oct 28, 2026
564a30a
Align sidebar paging tests with coalesced refreshes
marksftw Oct 28, 2026
5588a07
Embed the sidebar arrow icon and correct reload test label
marksftw Oct 28, 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
12 changes: 11 additions & 1 deletion apps/maple-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,17 @@ Goose, with an independent dependency graph.
Renaming from its desktop or a paired controller updates connected views and
persists across restarts without changing the folder path. The owner supplies
the default workspace label too. Personal pins remain desktop preferences.
- Sessions grouped by project, with rename, archive, and restore.
- Sidebar organization by Project, Connection, Status, or In one list, with
Last updated or Manual order for tasks. Connection becomes available after
another computer is added, including while it is offline. Project views keep
known empty projects visible. Project and connection order stays stable;
dragging rearranges the sidebar without changing a task's project, computer
or status. Dragging a task silently selects Manual order, which persists
across grouping changes and restarts; new tasks appear at the top. Pinned
tasks sit above the grouping header; their own drag order does not change
the position they return to when unpinned. Sidebar menus open toward the
main pane so the list stays visible. Tasks also support rename, archive,
and restore. Unknown or malformed View preferences fall back to defaults.
- Settings: General (web tools, appearance, tool call details, desktop
notifications, tool call summaries, composer Vim, application Vim, and the
speech voice and speed), Keyboard Shortcuts, System prompt, Integrations
Expand Down
1 change: 1 addition & 0 deletions apps/maple-agent/app/assets/icons/arrow-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/maple-agent/app/assets/icons/list-filter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/maple-agent/app/assets/icons/monitor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/maple-agent/app/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ macro_rules! assets {
assets!(
"icons/archive-restore.svg",
"icons/archive.svg",
"icons/arrow-down.svg",
"icons/arrow-left.svg",
"icons/arrow-up.svg",
"icons/brain.svg",
Expand All @@ -34,13 +35,15 @@ assets!(
"icons/folder.svg",
"icons/globe.svg",
"icons/image.svg",
"icons/list-filter.svg",
"icons/loader-circle.svg",
"icons/lock.svg",
"icons/mic.svg",
"icons/maple-mark.svg",
"icons/maple-wordmark.svg",
"icons/maximize-2.svg",
"icons/minimize-2.svg",
"icons/monitor.svg",
// OpenAI mark (simple-icons, CC0) for the Codex integration card.
"icons/openai-mark.svg",
"icons/paperclip.svg",
Expand Down
4 changes: 4 additions & 0 deletions apps/maple-agent/app/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ operations! {
add_word(word: (String)) -> bool;
get_remote_sidebars(user_id: (&str)) -> HashMap<String, crate::settings::RemoteSidebarSettings>;
set_remote_sidebar(user_id: (&str), computer: (String), patch: (serde_json::Value)) -> ();
get_sidebar_order(user_id: (&str)) -> crate::settings::SidebarOrder;
set_sidebar_order(user_id: (&str), order: (crate::settings::SidebarOrder)) -> ();
set_task_pinned(user_id: (&str), task: (&str), pinned: (bool)) -> ();
login(email: (String), password: (String)) -> AuthSession;
logout_and_clear(account_user: (&str)) -> ();
Expand Down Expand Up @@ -521,6 +523,8 @@ impl AgentBackend {
| "account"
| "get_remote_sidebars"
| "set_remote_sidebar"
| "get_sidebar_order"
| "set_sidebar_order"
) {
&controller.client
} else {
Expand Down
119 changes: 118 additions & 1 deletion apps/maple-agent/app/src/backend/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// there by design.
#![cfg_attr(not(feature = "desktop"), allow(dead_code))]

use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use std::process::Command;
Expand Down Expand Up @@ -63,6 +63,75 @@ pub struct AuthSession {
pub user_id: String,
}

fn validate_sidebar_order(order: &crate::settings::SidebarOrder) -> Result<(), String> {
const MAX_ITEMS: usize = 8192;
const MAX_CONNECTIONS: usize = 256;
const MAX_ROOT_BYTES: usize = 32768;
const MAX_TASK_ID_BYTES: usize = 256;
const MAX_ORDER_BYTES: usize = 2 * 1024 * 1024;
if order.projects.len() > MAX_ITEMS
|| order.tasks.len() > MAX_ITEMS
|| order.pinned_tasks.len() > MAX_ITEMS
|| order.connections.len() > MAX_CONNECTIONS
{
return Err("Sidebar order is too large".into());
}
if order.created_before_ms < 0 {
return Err("Invalid sidebar order cutoff".into());
}
let mut bytes = 0_usize;
let mut validate_computer = |computer: &Option<String>| -> Result<(), String> {
if let Some(computer) = computer {
if computer.is_empty() || computer.len() > 128 || computer.chars().any(char::is_control)
{
return Err("Invalid sidebar computer".into());
}
bytes = bytes.saturating_add(computer.len());
}
Ok(())
};
for (items, maximum, is_task) in [
(&order.projects, MAX_ROOT_BYTES, false),
(&order.tasks, MAX_TASK_ID_BYTES, true),
(&order.pinned_tasks, MAX_TASK_ID_BYTES, true),
] {
let mut seen = HashSet::new();
for item in items {
validate_computer(&item.computer)?;
if item.id.is_empty()
|| item.id.len() > maximum
|| item.id.contains('\0')
|| (is_task && item.id.chars().any(char::is_control))
|| !seen.insert(item)
{
return Err("Invalid sidebar order item".into());
}
}
}
let mut connections = HashSet::new();
for computer in &order.connections {
validate_computer(computer)?;
if !connections.insert(computer) {
return Err("Duplicate sidebar connection".into());
}
}
bytes = order
.projects
.iter()
.chain(&order.tasks)
.chain(&order.pinned_tasks)
.fold(bytes, |bytes, item| bytes.saturating_add(item.id.len()));
if bytes > MAX_ORDER_BYTES
|| serde_json::to_vec(order)
.map_err(|_| "Could not encode sidebar order".to_string())?
.len()
> MAX_ORDER_BYTES
{
return Err("Sidebar order is too large".into());
}
Ok(())
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SelectedProject {
Expand Down Expand Up @@ -1312,6 +1381,54 @@ impl LocalBackend {
.map_err(|_| "Could not save remote sidebar settings".to_string())?
}

/// Sidebar order is account-owned controller presentation state, independent
/// of the execution host's project ordering and navigation cursors.
pub async fn get_sidebar_order(
&self,
user_id: &str,
) -> Result<crate::settings::SidebarOrder, String> {
let (store, account) = self.presentation_account(user_id)?;
tokio::task::spawn_blocking(move || {
let order = match store
.account_setting(account, "sidebar.order")
.map_err(|error| error.to_string())?
{
Some(value) => serde_json::from_value(value)
.map_err(|_| "Saved sidebar order is invalid".to_string())?,
None => crate::settings::SidebarOrder::default(),
};
validate_sidebar_order(&order)?;
Ok(order)
})
.await
.map_err(|_| "Could not read sidebar order".to_string())?
}

pub async fn set_sidebar_order(
&self,
user_id: &str,
order: crate::settings::SidebarOrder,
) -> Result<(), String> {
validate_sidebar_order(&order)?;
// Host dispatch already holds the account read gate. Reuse the
// presentation writer gate without taking a nested account read lock.
let _gate = self.ui_settings_gate.lock().await;
let (store, account) = self.presentation_account(user_id)?;
tokio::task::spawn_blocking(move || {
store
.set_account_setting(
account,
"sidebar.order",
&serde_json::to_value(order)
.map_err(|_| "Could not encode sidebar order".to_string())?,
crate::settings::timestamp_ms(),
)
.map_err(|error| error.to_string())
})
.await
.map_err(|_| "Could not save sidebar order".to_string())?
}

pub async fn set_task_pinned(
&self,
user_id: &str,
Expand Down
Loading
Loading