From 4ebb933db93fffc384ee6fec06ab3db736118d22 Mon Sep 17 00:00:00 2001 From: Astrid Date: Fri, 4 Sep 2026 16:29:43 +0000 Subject: [PATCH 1/9] fix: simplify the update prompt Fixes #108. --- crates/app/src/dialogs/update.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/app/src/dialogs/update.rs b/crates/app/src/dialogs/update.rs index 47c1ecc0..22893557 100644 --- a/crates/app/src/dialogs/update.rs +++ b/crates/app/src/dialogs/update.rs @@ -38,12 +38,10 @@ pub(super) fn update_available( (_, Some(UpdateProgress::Installing)) => body .child("Installing the update\u{2026}".to_string()) .child(progress_bar(1.0)), - (Some(installer), None) => body.child(format!( - "Schist can download it ({}) and install it over this copy. It \ - restarts once the update is in place, and asks about any \ - unsaved documents on the way.", - megabytes(installer.size) - )), + // The buttons already make the available choices clear. Details + // about replacing the executable and restarting are implementation + // noise in what should be a quick update prompt. + (Some(_), None) => body, (None, None) => body.child( "This copy came from somewhere that owns it \u{2014} a package \ manager, an AppImage, a build of your own \u{2014} so it updates \ From b60b7e71e7452740ee5598ae4b9925c2fc5271d1 Mon Sep 17 00:00:00 2001 From: Astrid Date: Fri, 4 Sep 2026 16:29:43 +0000 Subject: [PATCH 2/9] fix: condense photo search model copy Fixes #109. --- crates/app/src/workspace/library_view.rs | 61 +++++++++++++++++++----- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/crates/app/src/workspace/library_view.rs b/crates/app/src/workspace/library_view.rs index 17b627c9..feccf90b 100644 --- a/crates/app/src/workspace/library_view.rs +++ b/crates/app/src/workspace/library_view.rs @@ -2298,6 +2298,24 @@ fn search_models_downloading(ws: &Workspace) -> bool { .any(|d| SEARCH_MODELS.contains(&d.id)) } +fn search_model_link( + id: &'static str, + label: &'static str, + url: &'static str, + cx: &mut Context, +) -> impl IntoElement { + div() + .id(id) + .cursor_pointer() + .text_color(gpui::rgb(crate::ui::palette().accent)) + .hover(|style| style.text_color(gpui::rgb(crate::ui::palette().accent_hover))) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |_ws, _event, _window, cx| cx.open_url(url)), + ) + .child(label) +} + /// The licences behind photo search, and the button that accepts them. /// One dialog for both models: they are downloaded as a pair. pub(crate) fn search_models_dialog(cx: &mut Context) -> impl IntoElement { @@ -2325,21 +2343,38 @@ pub(crate) fn search_models_dialog(cx: &mut Context) -> impl IntoElem "{} \u{b7} {:.0} MB", spec.name, spec.bytes as f64 / (1 << 20) as f64 - )))) - .child( - div() - .text_size(px(11.0)) - .text_color(gpui::rgb(crate::ui::palette().text_dim)) - .child(SharedString::from(spec.license)), - ) - .child( - div() - .text_size(px(11.0)) - .text_color(gpui::rgb(crate::ui::palette().text_dim)) - .child(SharedString::from(spec.note)), - ), + )))), ); } + body = body.child( + div() + .flex() + .flex_row() + .items_center() + .gap_1() + .text_size(px(11.0)) + .text_color(gpui::rgb(crate::ui::palette().text_dim)) + .child(search_model_link( + "mobileclip-source", + "MobileCLIP by Apple", + "https://github.com/apple/ml-mobileclip", + cx, + )) + .child("\u{b7}") + .child(search_model_link( + "mobileclip-export", + "ONNX export by Xenova", + "https://huggingface.co/Xenova/mobileclip_s0", + cx, + )) + .child("\u{b7}") + .child(search_model_link( + "mobileclip-license", + "License", + "https://github.com/apple/ml-mobileclip/blob/main/LICENSE", + cx, + )), + ); body = body.child( div() .pt_1() From f0e66e3a59240d7479383fbf746b7318c56af3f1 Mon Sep 17 00:00:00 2001 From: Astrid Date: Fri, 4 Sep 2026 16:29:43 +0000 Subject: [PATCH 3/9] fix: isolate AI panel memories Fixes #110. --- crates/app/src/ai/codex.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/app/src/ai/codex.rs b/crates/app/src/ai/codex.rs index 69032173..e4f183d4 100644 --- a/crates/app/src/ai/codex.rs +++ b/crates/app/src/ai/codex.rs @@ -87,6 +87,15 @@ fn run( // The bridge command and its token, as codex config: the value side of // each `-c` is TOML. builder = builder + // A canvas conversation is deliberately self-contained. The + // user's Codex memories belong to their coding work, and feeding + // them into this embedded assistant both confuses its role and + // can disclose context from an unrelated project. Do not consume + // those memories, and do not turn photo-editing conversations into + // inputs for future global memories either. + .config_override("features.memories", "false") + .config_override("memories.use_memories", "false") + .config_override("memories.generate_memories", "false") .config_override( "mcp_servers.schist.command", toml_string(&exe.display().to_string()), From 79dd2037994dc46d6588f21e5181ecb04395c613 Mon Sep 17 00:00:00 2001 From: Astrid Date: Fri, 4 Sep 2026 16:29:44 +0000 Subject: [PATCH 4/9] fix: keep size values on one line Fixes #111. --- crates/app/src/panels/sliders.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/app/src/panels/sliders.rs b/crates/app/src/panels/sliders.rs index 83300ec5..451d12d0 100644 --- a/crates/app/src/panels/sliders.rs +++ b/crates/app/src/panels/sliders.rs @@ -150,8 +150,11 @@ pub(super) fn slider( } row.child(track).child( div() - .w(px(34.0)) + // "180 px" is wider than the old 34px slot. Keep quantities + // on one line, including at the largest three-digit values. + .w(px(44.0)) .flex_none() + .whitespace_nowrap() .text_size(px(11.0)) .child(display), ) From e71ea58fd6b9dca96d85f86549e77e46697656a9 Mon Sep 17 00:00:00 2001 From: Astrid Date: Fri, 4 Sep 2026 16:30:10 +0000 Subject: [PATCH 5/9] feat: add a themed macOS title bar Fixes #112. --- crates/app/src/main.rs | 12 ++++++++-- crates/app/src/panels/mod.rs | 2 ++ crates/app/src/panels/titlebar.rs | 35 ++++++++++++++++++++++++++++++ crates/app/src/workspace/render.rs | 4 ++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 crates/app/src/panels/titlebar.rs diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 31d76ee9..30f48021 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -43,8 +43,8 @@ mod workspace; use actions::{HideApp, HideOthers, Quit, ShowAll}; use gpui::{ - px, size, App, AppContext as _, Application, AsyncApp, Bounds, WindowBounds, WindowHandle, - WindowOptions, + px, size, App, AppContext as _, Application, AsyncApp, Bounds, TitlebarOptions, WindowBounds, + WindowHandle, WindowOptions, }; use schist_plugin_api::{CodecPlugin, PluginManifest, PluginRegistry}; use std::cell::RefCell; @@ -343,6 +343,14 @@ fn main() { .open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), + titlebar: Some(TitlebarOptions { + title: Some("Schist".into()), + // AppKit keeps the traffic lights while allowing + // the workspace to paint a title bar matching its + // own light or dark theme. + appears_transparent: cfg!(target_os = "macos"), + ..Default::default() + }), ..Default::default() }, |_window, cx| { diff --git a/crates/app/src/panels/mod.rs b/crates/app/src/panels/mod.rs index 5cde6d2f..d7008baa 100644 --- a/crates/app/src/panels/mod.rs +++ b/crates/app/src/panels/mod.rs @@ -35,6 +35,7 @@ mod rulers; mod sliders; mod status; mod tabs; +mod titlebar; mod toolbar; #[cfg(not(target_arch = "wasm32"))] @@ -59,6 +60,7 @@ pub use rulers::*; pub use sliders::*; pub use status::*; pub use tabs::*; +pub use titlebar::*; pub use toolbar::*; fn swatch_hex(c: Rgba) -> gpui::Rgba { diff --git a/crates/app/src/panels/titlebar.rs b/crates/app/src/panels/titlebar.rs new file mode 100644 index 00000000..ae05ca5d --- /dev/null +++ b/crates/app/src/panels/titlebar.rs @@ -0,0 +1,35 @@ +//! Window title chrome painted into AppKit's transparent title-bar area. + +use super::*; + +/// A title bar that follows Schist's own theme. macOS keeps drawing and +/// operating the traffic-light controls above this row; the centred label +/// stays clear of them and follows the active document. +pub fn title_bar(ws: &Workspace) -> impl IntoElement { + let title: SharedString = match ws.doc.as_ref() { + Some(doc) if doc.dirty => format!("{} • — Schist", doc.title).into(), + Some(doc) => format!("{} — Schist", doc.title).into(), + None => "Schist".into(), + }; + + div() + .flex() + .flex_none() + .items_center() + .justify_center() + .h(px(28.0)) + .w_full() + .bg(gpui::rgb(palette().panel_bg)) + .border_b_1() + .border_color(gpui::rgb(palette().panel_edge)) + .child( + div() + .max_w(px(520.0)) + .overflow_hidden() + .whitespace_nowrap() + .text_ellipsis() + .text_size(px(11.0)) + .text_color(gpui::rgb(palette().text_dim)) + .child(title), + ) +} diff --git a/crates/app/src/workspace/render.rs b/crates/app/src/workspace/render.rs index 887c2f18..c9886974 100644 --- a/crates/app/src/workspace/render.rs +++ b/crates/app/src/workspace/render.rs @@ -806,6 +806,10 @@ impl Render for Workspace { })) .on_action(cx.listener(|ws, _: &NextTab, _w, cx| ws.cycle_tab(1, cx))) .on_action(cx.listener(|ws, _: &PrevTab, _w, cx| ws.cycle_tab(-1, cx))) + // With AppKit's title bar transparent, this row occupies the + // native drag/traffic-light area and follows the app theme. + // Other platforms retain their native window decorations. + .children((chrome && cfg!(target_os = "macos")).then(|| panels::title_bar(self))) .children(in_window_menus.then(|| panels::menu_bar(self, cx))) .children(editor_chrome.then(|| panels::tool_options_bar(self, cx))) .children(editor_chrome.then(|| panels::tab_bar(self, cx))) From b38908ce70cf67c3a6c880ac08d41d6dbdc19e8e Mon Sep 17 00:00:00 2001 From: Astrid Date: Fri, 4 Sep 2026 16:30:21 +0000 Subject: [PATCH 6/9] fix: keep the text caret visible Fixes #113. --- crates/app/src/workspace/mod.rs | 3 +++ crates/app/src/workspace/render.rs | 21 +++++++++++++++++++++ crates/plugin-api/src/lib.rs | 3 +++ plugins/tools-type/src/lib.rs | 2 +- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/app/src/workspace/mod.rs b/crates/app/src/workspace/mod.rs index ddf0417b..fcef9600 100644 --- a/crates/app/src/workspace/mod.rs +++ b/crates/app/src/workspace/mod.rs @@ -1496,6 +1496,9 @@ pub struct PaintJob { highlights: Vec>, outlines: Vec<(Bounds, gpui::Hsla)>, polylines: Vec<(Vec>, gpui::Hsla)>, + /// Text carets, painted as a dark stroke under a light one so either + /// half contrasts with the artwork below it. + carets: Vec>>, /// Marching-ants dashes. ants: Ants, circles: Vec>, diff --git a/crates/app/src/workspace/render.rs b/crates/app/src/workspace/render.rs index c9886974..c23aa565 100644 --- a/crates/app/src/workspace/render.rs +++ b/crates/app/src/workspace/render.rs @@ -267,6 +267,9 @@ impl Workspace { gpui::rgb(0xFFFFFF).into(), )); } + Overlay::Caret { x1, y1, x2, y2 } => { + job.carets.push(vec![to_screen(x1, y1), to_screen(x2, y2)]); + } Overlay::Circle { cx: ccx, cy, r } => { let d = r * 2.0 * zoom; job.circles.push(Bounds { @@ -493,6 +496,24 @@ impl Workspace { window.paint_path(path, color); } } + // A white hairline disappears on white text or a + // pale photograph. The dark three-pixel underlay + // leaves a one-pixel rim around the white centre, + // giving the insertion caret contrast everywhere. + for pts in job.carets { + for (width, color) in + [(3.0, gpui::rgb(0x000000)), (1.0, gpui::rgb(0xFFFFFF))] + { + let mut pb = PathBuilder::stroke(px(width)); + pb.move_to(pts[0]); + for p in &pts[1..] { + pb.line_to(*p); + } + if let Ok(path) = pb.build() { + window.paint_path(path, color); + } + } + } for bounds in job.circles { let r = bounds.size.width / 2.0; window.paint_quad(gpui::quad( diff --git a/crates/plugin-api/src/lib.rs b/crates/plugin-api/src/lib.rs index e19c119a..f78f5b76 100644 --- a/crates/plugin-api/src/lib.rs +++ b/crates/plugin-api/src/lib.rs @@ -123,6 +123,9 @@ pub enum Overlay { Circle { cx: f32, cy: f32, r: f32 }, /// Straight line segment. Line { x1: f32, y1: f32, x2: f32, y2: f32 }, + /// Text insertion caret. The host gives this a contrasting outline so + /// it stays visible over both light and dark artwork. + Caret { x1: f32, y1: f32, x2: f32, y2: f32 }, /// A note's pin, filled in the note's own colour. Drawn at a fixed /// *screen* size like Photoshop's, so a note stays findable and /// clickable whether the document is at 5% or 1600%. diff --git a/plugins/tools-type/src/lib.rs b/plugins/tools-type/src/lib.rs index 69d7b05e..b341d7dd 100644 --- a/plugins/tools-type/src/lib.rs +++ b/plugins/tools-type/src/lib.rs @@ -857,7 +857,7 @@ impl ToolPlugin for TypeTool { if let Some(caret) = schist_text_engine::caret_at(spec, session.caret) { let x = ox + caret.x; let y = oy + caret.top; - out.push(Overlay::Line { + out.push(Overlay::Caret { x1: x, y1: y, x2: x, From 74f2186b920772ee525437484f18b70402234e10 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 4 Sep 2026 17:49:16 +0100 Subject: [PATCH 7/9] fix: keep size info here --- crates/app/src/dialogs/update.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/app/src/dialogs/update.rs b/crates/app/src/dialogs/update.rs index 22893557..2b741e59 100644 --- a/crates/app/src/dialogs/update.rs +++ b/crates/app/src/dialogs/update.rs @@ -38,9 +38,10 @@ pub(super) fn update_available( (_, Some(UpdateProgress::Installing)) => body .child("Installing the update\u{2026}".to_string()) .child(progress_bar(1.0)), - // The buttons already make the available choices clear. Details - // about replacing the executable and restarting are implementation - // noise in what should be a quick update prompt. + (Some(installer), None) => body.child(format!( + "Schist can download it ({}) and install it over this copy.", + megabytes(installer.size) + )), (Some(_), None) => body, (None, None) => body.child( "This copy came from somewhere that owns it \u{2014} a package \ From c37cde03c555c8a05b57dd6aa467512e782dacba Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 4 Sep 2026 18:03:06 +0100 Subject: [PATCH 8/9] fix: clippy error --- crates/app/src/dialogs/update.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/app/src/dialogs/update.rs b/crates/app/src/dialogs/update.rs index 2b741e59..0025f34c 100644 --- a/crates/app/src/dialogs/update.rs +++ b/crates/app/src/dialogs/update.rs @@ -38,11 +38,10 @@ pub(super) fn update_available( (_, Some(UpdateProgress::Installing)) => body .child("Installing the update\u{2026}".to_string()) .child(progress_bar(1.0)), - (Some(installer), None) => body.child(format!( + (Some(_), None) => body.child(format!( "Schist can download it ({}) and install it over this copy.", megabytes(installer.size) )), - (Some(_), None) => body, (None, None) => body.child( "This copy came from somewhere that owns it \u{2014} a package \ manager, an AppImage, a build of your own \u{2014} so it updates \ From 4b6c6404f3a560b114f35d17fcf3b8e420d8ffaf Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 4 Sep 2026 18:03:43 +0100 Subject: [PATCH 9/9] fix: I am brainrotting today --- crates/app/src/dialogs/update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/app/src/dialogs/update.rs b/crates/app/src/dialogs/update.rs index 0025f34c..5213d530 100644 --- a/crates/app/src/dialogs/update.rs +++ b/crates/app/src/dialogs/update.rs @@ -38,7 +38,7 @@ pub(super) fn update_available( (_, Some(UpdateProgress::Installing)) => body .child("Installing the update\u{2026}".to_string()) .child(progress_bar(1.0)), - (Some(_), None) => body.child(format!( + (Some(installer), None) => body.child(format!( "Schist can download it ({}) and install it over this copy.", megabytes(installer.size) )),