From 428e2df40a841706558c44d6138dfcb3ac864124 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 9 Nov 2026 05:27:45 +0100 Subject: [PATCH] Add share target, prompt templates, command palette and session organisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- core/src/types.rs | 9 + server/src/api.rs | 31 +++ server/src/cli_api.rs | 44 ++++ server/src/jobs.rs | 3 + server/src/runner.rs | 3 + web/src/App.tsx | 8 +- web/src/components/ChatView.tsx | 3 +- web/src/components/Composer.tsx | 15 ++ web/src/components/Icon.tsx | 3 + web/src/components/NewSession.tsx | 7 +- web/src/components/Palette.tsx | 148 ++++++++++++ web/src/components/Settings.tsx | 7 +- web/src/components/ShareTarget.tsx | 59 +++++ web/src/components/Sidebar.tsx | 168 +++++++++++--- web/src/components/TemplatePicker.tsx | 123 ++++++++++ web/src/components/settings/Templates.tsx | 79 +++++++ web/src/i18n/de.ts | 45 ++++ web/src/i18n/en.ts | 45 ++++ web/src/main.tsx | 2 + web/src/share.ts | 35 +++ web/src/styles.css | 264 ++++++++++++++++++++++ web/src/templates.ts | 29 +++ web/src/types.ts | 3 + web/src/whatsnew.ts | 4 + web/vite.config.ts | 2 + 25 files changed, 1106 insertions(+), 33 deletions(-) create mode 100644 web/src/components/Palette.tsx create mode 100644 web/src/components/ShareTarget.tsx create mode 100644 web/src/components/TemplatePicker.tsx create mode 100644 web/src/components/settings/Templates.tsx create mode 100644 web/src/share.ts create mode 100644 web/src/templates.ts diff --git a/core/src/types.rs b/core/src/types.rs index a81cf45..1462df1 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -133,6 +133,15 @@ pub struct Session { /// Isolated git worktree of this session: {repo, path, branch, base}. #[serde(default, skip_serializing_if = "Option::is_none")] pub worktree: Option, + /// Shown at the top of the session list. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub pinned: bool, + /// Hidden from the session list (still searchable). + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub archived: bool, + /// User-defined group in the session list. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub folder: Option, } impl Session { diff --git a/server/src/api.rs b/server/src/api.rs index 604df6a..fd3151e 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -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, State(app): State, State(app): State, permission_mode: Option, clean_room: Option, + pinned: Option, + archived: Option, + /// "" removes the folder. + folder: Option, } async fn update_session(Extension(user): Extension, State(app): State, Path(id): Path, Json(body): Json) -> 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, State(app): State(); + 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())); diff --git a/server/src/cli_api.rs b/server/src/cli_api.rs index 35bf099..d6985e6 100644 --- a/server/src/cli_api.rs +++ b/server/src/cli_api.rs @@ -248,3 +248,47 @@ pub async fn put_memory(Extension(user): Extension, State(app): State, State(app): State) -> ApiResult { + let personal: Vec