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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 05:27:45 +01:00
1 parent b5de531d2e
commit 428e2df40a
25 files changed
+1106 -33

No files matched your search

+44
View File
@@ -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)))
}