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

+31
View File
@@ -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()));