Add share target, prompt templates, command palette and session organisation
- PWA share target: shared text/links start a new chat or go into a
recent session (chooser dialog)
- Prompt templates (personal + global for admins) with {{placeholders}}
that are asked for on insert; managed under Settings → Templates
- Ctrl/Cmd+K command palette; Alt+N new session, Alt+Up/Down switch
sessions, Ctrl+, settings
- Sessions can be pinned, archived and filed into folders; the list can
be grouped by date or by project, groups are collapsible
- Fix: the "What's new" dialog was never shown to signed-in users
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5de531d2e
commit
428e2df40a
25 files changed
+1106
-33
No files matched your search
@@ -129,6 +129,8 @@ pub fn router(app: AppState) -> Router {
|
||||
.route("/account/git", get(get_git_settings).put(put_git_settings))
|
||||
.route("/account/prefs", get(crate::cli_api::get_prefs).put(crate::cli_api::put_prefs))
|
||||
.route("/account/memory", get(crate::cli_api::get_memory).put(crate::cli_api::put_memory))
|
||||
.route("/templates", get(crate::cli_api::get_templates))
|
||||
.route("/templates/{scope}", put(crate::cli_api::put_templates))
|
||||
.route("/cli/config", get(crate::cli_api::config))
|
||||
.route("/cli/skill/{name}", get(crate::cli_api::skill))
|
||||
.route("/cli/tool", post(crate::cli_api::tool))
|
||||
@@ -471,6 +473,9 @@ async fn create_session(Extension(user): Extension<User>, State(app): State<AppS
|
||||
clean_room: body.clean_room,
|
||||
phase: None,
|
||||
worktree: None,
|
||||
pinned: false,
|
||||
archived: false,
|
||||
folder: None,
|
||||
};
|
||||
let s = app.save_session(s)?;
|
||||
Ok(Json(json!(s)))
|
||||
@@ -617,6 +622,8 @@ async fn fork_session(Extension(user): Extension<User>, State(app): State<AppSta
|
||||
last_usage: None,
|
||||
phase: None,
|
||||
worktree: None,
|
||||
pinned: false,
|
||||
archived: false,
|
||||
..src.clone()
|
||||
};
|
||||
let fork = app.save_session(fork)?;
|
||||
@@ -673,10 +680,16 @@ struct UpdateSession {
|
||||
agent: Option<String>,
|
||||
permission_mode: Option<String>,
|
||||
clean_room: Option<bool>,
|
||||
pinned: Option<bool>,
|
||||
archived: Option<bool>,
|
||||
/// "" removes the folder.
|
||||
folder: Option<String>,
|
||||
}
|
||||
|
||||
async fn update_session(Extension(user): Extension<User>, State(app): State<AppState>, Path(id): Path<String>, Json(body): Json<UpdateSession>) -> ApiResult {
|
||||
let mut s = writable(&app, &user, &id)?;
|
||||
// Organising the list must not move the session to "today".
|
||||
let organise_only = body.title.is_none() && body.model.is_none() && body.agent.is_none() && body.clean_room.is_none() && body.permission_mode.is_none();
|
||||
if let Some(t) = body.title {
|
||||
s.title = t;
|
||||
}
|
||||
@@ -690,6 +703,24 @@ async fn update_session(Extension(user): Extension<User>, State(app): State<AppS
|
||||
if let Some(c) = body.clean_room {
|
||||
s.clean_room = Some(c);
|
||||
}
|
||||
if let Some(p) = body.pinned {
|
||||
s.pinned = p;
|
||||
}
|
||||
if let Some(a) = body.archived {
|
||||
s.archived = a;
|
||||
if a {
|
||||
s.pinned = false;
|
||||
}
|
||||
}
|
||||
if let Some(f) = body.folder {
|
||||
let f = f.trim().chars().take(60).collect::<String>();
|
||||
s.folder = (!f.is_empty()).then_some(f);
|
||||
}
|
||||
if organise_only {
|
||||
app.store.save_session(&s)?;
|
||||
app.emit(Event::SessionUpdated { session: s.clone() });
|
||||
return Ok(Json(json!(s)));
|
||||
}
|
||||
if let Some(p) = body.permission_mode {
|
||||
if !crate::permission::MODES.contains(&p.as_str()) {
|
||||
return Err(ApiError(StatusCode::BAD_REQUEST, "invalid permission mode".into()));
|
||||
|
||||
@@ -248,3 +248,47 @@ pub async fn put_memory(Extension(user): Extension<User>, State(app): State<AppS
|
||||
app.store.set_user_setting(&user.id, "memory", &b.memory)?;
|
||||
Ok(Json(json!({"memory": b.memory})))
|
||||
}
|
||||
|
||||
// --- prompt templates ---------------------------------------------------------
|
||||
|
||||
/// Owner id of the templates every user sees (edited by admins).
|
||||
const GLOBAL_TEMPLATES: &str = "*";
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, Deserialize, Default)]
|
||||
pub struct Template {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub async fn get_templates(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
|
||||
let personal: Vec<Template> = app.store.user_setting(&user.id, "templates")?;
|
||||
let global: Vec<Template> = app.store.user_setting(GLOBAL_TEMPLATES, "templates")?;
|
||||
Ok(Json(json!({"personal": personal, "global": global, "can_edit_global": user.is_admin()})))
|
||||
}
|
||||
|
||||
pub async fn put_templates(Extension(user): Extension<User>, State(app): State<AppState>, Path(scope): Path<String>, Json(list): Json<Vec<Template>>) -> ApiResult {
|
||||
let owner = match scope.as_str() {
|
||||
"personal" => user.id.clone(),
|
||||
"global" => {
|
||||
crate::auth::require_admin(&user)?;
|
||||
GLOBAL_TEMPLATES.to_string()
|
||||
}
|
||||
_ => return Err(ApiError(StatusCode::NOT_FOUND, "unknown scope".into())),
|
||||
};
|
||||
if list.len() > 200 || list.iter().any(|t| t.name.trim().is_empty() || t.name.len() > 100 || t.text.len() > 20_000) {
|
||||
return Err(ApiError(StatusCode::BAD_REQUEST, "templates need a name (max. 100 characters) and at most 20,000 characters of text".into()));
|
||||
}
|
||||
let list: Vec<Template> = list
|
||||
.into_iter()
|
||||
.map(|mut t| {
|
||||
if t.id.is_empty() {
|
||||
t.id = uuid::Uuid::new_v4().simple().to_string();
|
||||
}
|
||||
t.name = t.name.trim().to_string();
|
||||
t
|
||||
})
|
||||
.collect();
|
||||
app.store.set_user_setting(&owner, "templates", &list)?;
|
||||
Ok(Json(json!(list)))
|
||||
}
|
||||
@@ -161,6 +161,9 @@ pub async fn run_with(app: &Arc<App>, job: &Job, trigger: &str, payload: Option<
|
||||
clean_room: None,
|
||||
phase: None,
|
||||
worktree: None,
|
||||
pinned: false,
|
||||
archived: false,
|
||||
folder: None,
|
||||
};
|
||||
let session = app.save_session(session)?;
|
||||
app.store.job_started(&job.id, &session.id, now)?;
|
||||
|
||||
@@ -700,6 +700,9 @@ fn create_child(app: &Arc<App>, ctx: &ToolCtx, agent_name: &str, description: &s
|
||||
clean_room: parent.clean_room,
|
||||
phase: None,
|
||||
worktree: None,
|
||||
pinned: false,
|
||||
archived: false,
|
||||
folder: None,
|
||||
};
|
||||
let child = app.save_session(child)?;
|
||||
let msg = Message::new(Role::User, vec![Part::Text { text: prompt }]);
|
||||
|
||||
Reference in new issue
Block a user