From 7af630c5c97fa25f36258e8ad15c9dca23181c26 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:17:50 -0400 Subject: [PATCH 01/12] Support optional SDXL sampling steps, seed and negative prompt with validation --- src-tauri/src/diffusion_runtime.rs | 78 +++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/diffusion_runtime.rs b/src-tauri/src/diffusion_runtime.rs index 66ad05f..7d81b68 100644 --- a/src-tauri/src/diffusion_runtime.rs +++ b/src-tauri/src/diffusion_runtime.rs @@ -193,6 +193,39 @@ struct VideoRenderProfile { vae_on_cpu: bool, } +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageRenderOptions { + pub negative_prompt: Option, + pub seed: Option, + pub steps: Option, +} + +impl ImageRenderOptions { + pub fn validate(&self) -> Result<(), AppError> { + if self + .negative_prompt + .as_deref() + .is_some_and(|text| text.chars().count() > MAX_PROMPT_CHARS) + { + return Err(AppError::ArtifactGenerationFailed(format!( + "negative prompt is too long; maximum is {MAX_PROMPT_CHARS} characters" + ))); + } + if self.steps.is_some_and(|steps| !(1..=50).contains(&steps)) { + return Err(AppError::ArtifactGenerationFailed( + "image sampling steps must be between 1 and 50".to_string(), + )); + } + if self.seed.is_some_and(|seed| !(0..=2_147_483_647).contains(&seed)) { + return Err(AppError::ArtifactGenerationFailed( + "image seed must be between 0 and 2147483647".to_string(), + )); + } + Ok(()) + } +} + // Keep the render's independent progress callback alongside its existing inputs. #[allow(clippy::too_many_arguments)] pub async fn generate_image( @@ -203,9 +236,11 @@ pub async fn generate_image( prompt: &str, output_path: &Path, use_cpu: bool, + options: &ImageRenderOptions, on_stage: &(dyn Fn(&'static str) + Send + Sync), on_step: &(dyn Fn(u32, u32) + Send + Sync), ) -> Result<(), AppError> { + options.validate()?; let prompt = prompt.trim(); if prompt.is_empty() { return Err(AppError::ArtifactGenerationFailed( @@ -230,7 +265,10 @@ pub async fn generate_image( let runtime = ensure_runtime(root, client, hardware, use_cpu).await?; on_stage("rendering"); let cli_path = root.resolve_relative(&runtime.cli_path)?; - let profile = render_profile(hardware, &runtime.backend); + let mut profile = render_profile(hardware, &runtime.backend); + if let Some(steps) = options.steps { + profile.steps = steps; + } if output_path.exists() { fs::remove_file(output_path)?; @@ -260,6 +298,14 @@ pub async fn generate_image( .stderr(Stdio::piped()) .kill_on_drop(true); + if let Some(negative) = options.negative_prompt.as_deref().map(str::trim) { + if !negative.is_empty() { + command.arg("--negative-prompt").arg(negative); + } + } + if let Some(seed) = options.seed { + command.arg("--seed").arg(seed.to_string()); + } if profile.clip_on_cpu { command.arg("--clip-on-cpu"); } @@ -1378,6 +1424,36 @@ mod tests { assert_eq!(automatic[0].0, BackendKind::Cuda); } + #[test] + fn image_options_validate_requested_seed_steps_and_prompt() { + assert!(ImageRenderOptions::default().validate().is_ok()); + assert!(ImageRenderOptions { + seed: Some(42), + steps: Some(20), + negative_prompt: Some("blurry, low resolution".to_string()), + } + .validate() + .is_ok()); + assert!(ImageRenderOptions { + seed: Some(-1), + ..ImageRenderOptions::default() + } + .validate() + .is_err()); + assert!(ImageRenderOptions { + steps: Some(51), + ..ImageRenderOptions::default() + } + .validate() + .is_err()); + assert!(ImageRenderOptions { + negative_prompt: Some("x".repeat(MAX_PROMPT_CHARS + 1)), + ..ImageRenderOptions::default() + } + .validate() + .is_err()); + } + #[test] fn sampling_progress_ignores_unrelated_logs() { assert_eq!(parse_sampling_step("Sampling step 3/20", 20), Some(3)); From bd781f5791fcc7680aee93b3f660c458302671cf Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:18:13 -0400 Subject: [PATCH 02/12] Forward validated SDXL image options through Tauri generation command --- src-tauri/src/lib_legacy.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib_legacy.rs b/src-tauri/src/lib_legacy.rs index 70326db..d9c9837 100644 --- a/src-tauri/src/lib_legacy.rs +++ b/src-tauri/src/lib_legacy.rs @@ -639,8 +639,18 @@ async fn create_generation_artifact( kind: String, prompt: String, use_cpu: Option, + image_options: Option, state: State<'_, AppState>, ) -> Result { + if kind != "image" && image_options.is_some() { + return Err(app_error::AppError::internal( + "image rendering options are only available for images", + )); + } + let image_options = image_options.unwrap_or_default(); + if kind == "image" { + image_options.validate()?; + } if use_cpu == Some(true) && kind != "image" { return Err(app_error::AppError::internal( "CPU-only retry is available for images only", @@ -730,7 +740,7 @@ async fn create_generation_artifact( }; report_stage("preparing"); generate_local_media_artifact( - &state, &kind, &prompt, &path, use_cpu.unwrap_or(false), &report_stage, &report_step + &state, &kind, &prompt, &path, use_cpu.unwrap_or(false), &image_options, &report_stage, &report_step ).await }; tokio::pin!(render); @@ -762,7 +772,7 @@ async fn create_generation_artifact( } } else { generate_local_media_artifact( - &state, &kind, &prompt, &path, use_cpu.unwrap_or(false), &report_stage, &report_step + &state, &kind, &prompt, &path, use_cpu.unwrap_or(false), &image_options, &report_stage, &report_step ).await }; // A cancellation arriving just as inference finishes must not publish a ready image. @@ -811,6 +821,7 @@ async fn generate_local_media_artifact( prompt: &str, path: &std::path::Path, use_cpu: bool, + image_options: &diffusion_runtime::ImageRenderOptions, on_stage: &(dyn Fn(&'static str) + Send + Sync), on_step: &(dyn Fn(u32, u32) + Send + Sync), ) -> Result<(), app_error::AppError> { @@ -837,6 +848,7 @@ async fn generate_local_media_artifact( prompt, path, use_cpu, + image_options, on_stage, on_step, ) From 64b4b5a17441eefa9b435821943d922960b52fa3 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:18:30 -0400 Subject: [PATCH 03/12] Preserve default SDXL smoke behavior with optional image controls --- src-tauri/examples/media_smoke.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/examples/media_smoke.rs b/src-tauri/examples/media_smoke.rs index 41f0a5a..e140f87 100644 --- a/src-tauri/examples/media_smoke.rs +++ b/src-tauri/examples/media_smoke.rs @@ -214,6 +214,7 @@ async fn smoke_canvas( "A simple red circle centered on a clean white background, minimal composition.", &output, false, // Standard smoke test keeps the platform's preferred renderer. + &diffusion_runtime::ImageRenderOptions::default(), &|stage| println!("[Canvas] {stage}"), &|step, total| println!("[Canvas] sampling step {step}/{total}"), ) From b26ffd0f10081325ef991cd0f4b6f8eb4a04c558 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:19:04 -0400 Subject: [PATCH 04/12] Pass optional SDXL render controls through src/types.ts --- src/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/types.ts b/src/types.ts index 168fe59..b969005 100644 --- a/src/types.ts +++ b/src/types.ts @@ -330,6 +330,13 @@ export type ArtifactStatus = "generating" | "ready" | "failed"; export type ImageRenderStage = "queued" | "preparing" | "rendering" | "validating"; +/** Leave seed and steps unset to use the current hardware-aware renderer defaults. */ +export interface ImageGenerationOptions { + negativePrompt: string; + seed: number | null; + steps: number | null; +} + export interface ImageRenderProgress { artifactId: string; stage: ImageRenderStage; From 80d494b9c2d41e9ba63e8d7b55f9c4c6f2a7462f Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:19:09 -0400 Subject: [PATCH 05/12] Pass optional SDXL render controls through src/api.ts --- src/api.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index f3d1b3a..67e587c 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,7 @@ import { invoke } from "@tauri-apps/api/core"; import type { Artifact, + ImageGenerationOptions, LlamaRuntimeStatus, Message, RuntimeInventory, @@ -368,11 +369,12 @@ export const api = { kind: "image" | "video" | "voice", prompt: string, useCpu = false, + imageOptions?: ImageGenerationOptions, ): Promise => { if (kind === "voice" && isTauri && (await messageUsesSoundscape(conversationId, messageId))) { return createSoundscapeArtifact(conversationId, messageId, prompt); } - return legacyApi.createGenerationArtifact(conversationId, messageId, kind, prompt, useCpu); + return legacyApi.createGenerationArtifact(conversationId, messageId, kind, prompt, useCpu, imageOptions); }, googleWorkspaceStatus: () => connectedInvoke("google_workspace_status"), connectGoogleWorkspace: () => connectedInvoke("connect_google_workspace"), From 67c2cf18491195ebbb896cbe6390b6377e3d3c98 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:19:13 -0400 Subject: [PATCH 06/12] Pass optional SDXL render controls through src/api_legacy.ts --- src/api_legacy.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/api_legacy.ts b/src/api_legacy.ts index 38e2382..b61739b 100644 --- a/src/api_legacy.ts +++ b/src/api_legacy.ts @@ -1,6 +1,7 @@ import { invoke } from "@tauri-apps/api/core"; import type { Artifact, + ImageGenerationOptions, ArtifactKind, Conversation, AppPreferences, @@ -356,7 +357,15 @@ export const api = { kind: "image" | "video" | "voice", prompt: string, useCpu = false, - ) => call("create_generation_artifact", { conversationId, messageId, kind, prompt, useCpu }), + imageOptions?: ImageGenerationOptions, + ) => call("create_generation_artifact", { + conversationId, + messageId, + kind, + prompt, + useCpu, + imageOptions: kind === "image" ? imageOptions : undefined, + }), listArtifacts: (conversationId: string) => call("list_artifacts", { conversationId }), listLibraryEntries: () => call("list_library_entries"), openArtifact: (artifactId: string) => call("open_artifact", { artifactId }), From a079486e65e2210934c5282d5be31b23cfd48aaa Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:19:57 -0400 Subject: [PATCH 07/12] Expose optional SDXL seed, sampling steps and negative prompt in chat composer --- src/components/Composer.tsx | 78 ++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/components/Composer.tsx b/src/components/Composer.tsx index c603056..212ba98 100644 --- a/src/components/Composer.tsx +++ b/src/components/Composer.tsx @@ -17,9 +17,9 @@ import { X, } from "lucide-react"; import type { ReactNode, RefObject } from "react"; -import type { ModelRecord, RuntimeInventory } from "../types"; +import type { ImageGenerationOptions, ModelRecord, RuntimeInventory } from "../types"; import { formatBytes } from "../lib/format"; -import type { AttachmentDraft } from "../lib/chat"; +import { inferChatMode, type AttachmentDraft } from "../lib/chat"; import { transcribeAudioBlob } from "../lib/media"; import { api } from "../api"; import { ModelSelector } from "./ModelSelector"; @@ -27,6 +27,8 @@ import { ModelSelector } from "./ModelSelector"; export function Composer(props: { prompt: string; setPrompt: (value: string) => void; + imageOptions: ImageGenerationOptions; + setImageOptions: (options: ImageGenerationOptions) => void; attachments: AttachmentDraft[]; enterToSend: boolean; streaming: boolean; @@ -53,6 +55,8 @@ export function Composer(props: { const audioPreviewUrlsRef = useRef(new Map()); const attachmentsRef = useRef(props.attachments); attachmentsRef.current = props.attachments; + const [imageSettingsOpen, setImageSettingsOpen] = useState(false); + const isImageRequest = inferChatMode(props.prompt, props.attachments) === "image"; const [recording, setRecording] = useState(false); const [transcribing, setTranscribing] = useState(false); const [micError, setMicError] = useState(null); @@ -257,6 +261,76 @@ export function Composer(props: { })} ) : null} + {isImageRequest ? ( +
+ + {imageSettingsOpen ? ( +
+ + + +

+ Leave seed and steps blank to keep the default renderer settings. +

+
+ ) : null} +
+ ) : null}
fileInputRef.current?.click()} From ad8aaf9929f2e7d299cbe3d0852dcff1d4dcc2a4 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:20:10 -0400 Subject: [PATCH 08/12] Pass image controls through regular and Work chat view --- src/components/ChatView.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx index e2297c6..34d36a5 100644 --- a/src/components/ChatView.tsx +++ b/src/components/ChatView.tsx @@ -18,6 +18,7 @@ import type { Artifact, ArtifactKind, ImageRenderProgress, + ImageGenerationOptions, Conversation, Message, ModelRecord, @@ -52,6 +53,8 @@ export function ChatView(props: { messages: Message[]; prompt: string; setPrompt: (value: string) => void; + imageOptions: ImageGenerationOptions; + setImageOptions: (options: ImageGenerationOptions) => void; attachments: AttachmentDraft[]; preferences: AppPreferences | null; activity: RealtimeActivity; @@ -113,6 +116,8 @@ export function ChatView(props: { Date: Tue, 20 Oct 2026 10:20:38 -0400 Subject: [PATCH 09/12] Use image settings for new requests and retries in both chat views --- src/App.tsx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 91ccba3..156838f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { api } from "./api"; import type { Artifact, ArtifactKind, + ImageGenerationOptions, ImageRenderProgress, Conversation, AppPreferences, @@ -103,6 +104,11 @@ export function App() { const [activeId, setActiveId] = useState(null); const [messages, setMessages] = useState([]); const [artifacts, setArtifacts] = useState([]); + const [imageOptions, setImageOptions] = useState({ + negativePrompt: "", + seed: null, + steps: null, + }); const [imageProgressByArtifact, setImageProgressByArtifact] = useState>({}); const [prompt, setPrompt] = useState(""); const [attachments, setAttachments] = useState([]); @@ -580,6 +586,8 @@ export function App() { assistant.id, generationKind, generationPrompt, + false, + generationKind === "image" ? imageOptions : undefined, ); setArtifacts((items) => upsertArtifactInList(items, artifact)); if (preferences?.openArtifactsAfterGeneration && artifact.status === "ready") { @@ -618,6 +626,7 @@ export function App() { attachments, conversations, editingMessageId, + imageOptions, messages, pendingModelId, preferences?.autoGenerateTitles, @@ -1043,7 +1052,14 @@ export function App() { ? imageRendererPrompt(source.content, precedingUser.content) : source.content; void api - .createGenerationArtifact(activeId, artifact.messageId, generationKind, generationPrompt, useCpu) + .createGenerationArtifact( + activeId, + artifact.messageId, + generationKind, + generationPrompt, + useCpu, + generationKind === "image" ? imageOptions : undefined, + ) .then((next) => { setArtifacts((items) => upsertArtifactInList(items, next)); if (preferences?.openArtifactsAfterGeneration && next.status === "ready") { @@ -1055,7 +1071,7 @@ export function App() { } void createArtifact(artifact.messageId, artifact.kind, source.content, artifact.name); }, - [activeId, createArtifact, messages, preferences?.openArtifactsAfterGeneration, showError], + [activeId, createArtifact, imageOptions, messages, preferences?.openArtifactsAfterGeneration, showError], ); const cancelImageArtifact = useCallback( @@ -1288,6 +1304,8 @@ export function App() { messages={messages} prompt={prompt} setPrompt={setPrompt} + imageOptions={imageOptions} + setImageOptions={setImageOptions} attachments={attachments} preferences={preferences} activity={activity} @@ -1344,6 +1362,8 @@ export function App() { messages={messages} prompt={prompt} setPrompt={setPrompt} + imageOptions={imageOptions} + setImageOptions={setImageOptions} attachments={attachments} preferences={preferences} activity={activity} From 3c54c7b2b00bf47cd2a1c3fc9e316f738a65128b Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:21:03 -0400 Subject: [PATCH 10/12] Style accessible image options in dark and light chat composers --- src/styles.css | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/styles.css b/src/styles.css index 988d794..d14b728 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1525,6 +1525,77 @@ button { box-shadow: 0 4px 12px rgb(0 0 0 / 12%); } +.composer-image-options { + display: flex; + flex-direction: column; + gap: 8px; +} + +.composer button.composer-image-options-toggle { + display: inline-flex; + align-self: flex-start; + width: auto; + height: auto; + min-height: 27px; + padding: 4px 10px; + border-radius: 999px; + background: var(--surface-2); + color: var(--text); + font-size: 12px; + font-weight: 600; +} + +.composer-image-options-fields { + display: grid; + grid-template-columns: minmax(95px, 1fr) minmax(110px, 1fr) minmax(160px, 2fr); + align-items: end; + gap: 8px; + padding: 0 5px 4px; +} + +.composer-image-options-fields label { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + font-size: 11px; + color: var(--muted); +} + +.composer-image-options-fields input { + min-width: 0; + width: 100%; + padding: 8px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + color: var(--text); + font: inherit; + font-size: 12px; +} + +.composer-image-options-fields input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +.composer-image-options-note { + grid-column: 1 / -1; + margin: 0; + font-size: 11px; + color: var(--muted); +} + +@media (max-width: 640px) { + .composer-image-options-fields { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .composer-image-negative { + grid-column: 1 / -1; + } +} + .composer-row { display: flex; align-items: center; From cedc046e17f1101b6cef5cf12fb2e3dce2414636 Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 10:22:08 -0400 Subject: [PATCH 11/12] Align generation retry call with existing client style --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 156838f..b0b3258 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1052,7 +1052,7 @@ export function App() { ? imageRendererPrompt(source.content, precedingUser.content) : source.content; void api - .createGenerationArtifact( + .createGenerationArtifact( activeId, artifact.messageId, generationKind, From 174c9e229f5ad997afda70ab9e30e83cb563cbef Mon Sep 17 00:00:00 2001 From: Shahanur Islam Shagor Date: Tue, 20 Oct 2026 12:56:02 -0400 Subject: [PATCH 12/12] Format SDXL seed validation for cross-platform Rust checks --- src-tauri/src/diffusion_runtime.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/diffusion_runtime.rs b/src-tauri/src/diffusion_runtime.rs index 7d81b68..0cd5df8 100644 --- a/src-tauri/src/diffusion_runtime.rs +++ b/src-tauri/src/diffusion_runtime.rs @@ -217,7 +217,10 @@ impl ImageRenderOptions { "image sampling steps must be between 1 and 50".to_string(), )); } - if self.seed.is_some_and(|seed| !(0..=2_147_483_647).contains(&seed)) { + if self + .seed + .is_some_and(|seed| !(0..=2_147_483_647).contains(&seed)) + { return Err(AppError::ArtifactGenerationFailed( "image seed must be between 0 and 2147483647".to_string(), ));