From 9ce20baa81fc5e1ac14411ceb09f0bdec2a0bdee Mon Sep 17 00:00:00 2001 From: Vanyo Vanev Date: Fri, 11 Sep 2026 21:11:47 +0300 Subject: [PATCH 1/4] Refuse to quit with attached drives and reserve remembered network letters Tauri destroys the window after a JavaScript close-request callback unless it prevents the default, even when Rust has rejected the close. Quitting with a drive attached therefore hid the window while ShellCanvas and the Drive Bridge kept running. The frontend now always prevents the automatic destroy and asks request_app_close, which refuses while drives run and marks the desktop as closing under the attachment lock, so a waiting attachment cannot start after the window is gone. On Windows, attachment also treats letters of remembered, disconnected network drives as reserved (WNetGetConnectionW). Mount cleanup still checks only active mounts, so such a letter is not mistaken for a live bridge. The attach, blocked detach and quit, detach and quit checklist was confirmed manually on Windows with an isolated profile. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 + docs/filesystem-integration-progress.md | 78 ++++++++++++++++++++----- src-tauri/Cargo.toml | 2 +- src-tauri/src/drive_mappings.rs | 7 ++- src-tauri/src/lib.rs | 26 ++++++++- src-tauri/src/local_mounts.rs | 75 ++++++++++++++++++++++-- src/App.tsx | 11 +++- 7 files changed, 177 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68705c4..a734300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ ShellCanvas follows semantic versioning for published SDK packages. Desktop rele - Changed: **Apps** is now **App Manager**, a store-style manager with tiles, search, per-app details (status, source, approved access, update, disable and confirmed removal) and an **Add apps** page for GitHub and package-file installs. - Added (app packages and SDK): optional one-line `description` and embedded `icon` fields. `shellcanvas-app build` embeds a PNG, JPEG, WebP or SVG icon of at most 256 KiB, and `shellcanvas-app init --description` and `repository` fill descriptions. Apps without an icon get a generic one. Packages that use the new fields need a desktop newer than 0.1.0; existing packages are unchanged. - Fixed: installed apps opened in a workspace without an SSH target (connection adapters, the preview) now finish starting. Their environment event carried an undefined `host.target`, which the event journal rejected; the field is now omitted. +- Fixed: closing ShellCanvas while a local drive is attached no longer hides the window and leaves ShellCanvas and the Drive Bridge running in the background. Quitting is refused with a message until drives are detached, and no drive can start attaching once the window is closing. +- Fixed (Windows): drive letters held by remembered but disconnected network drives are no longer offered or accepted for drive attachment. ## 0.1.1 diff --git a/docs/filesystem-integration-progress.md b/docs/filesystem-integration-progress.md index fc8069c..3527d0c 100644 --- a/docs/filesystem-integration-progress.md +++ b/docs/filesystem-integration-progress.md @@ -2,6 +2,14 @@ Updated 2026-09-11. Goal remains active. This is not a release/completion claim. +Resumed after the Codex restart. The corrected isolated desktop build completed +successfully (3m 14s; executable timestamp 2026-09-11 01:43:23 local). After the +detach fix, the user confirmed the Windows flow worked end to end. This is manual, +user-reported acceptance of the attach/detach/quit checklist. Further testing is +stopped to conserve their OpenAI weekly quota; no additional CI was triggered. The +disposable test folder is `/tmp/shellcanvas-quit-check-Ijl8MnLz`. The desktop +changes are committed; the detailed local handoff is `.local/drive-bridge-resume.md`. + ## Accepted scope Implement optional core filesystem operations and machinery, with Windows @@ -12,6 +20,44 @@ Include the dependencies' licensing/distribution limitations in that app. ## Current implementation +- Manual Windows testing found detach stuck on two handles on the empty fixture. + The bridge counted passive directory browsing as busy. Read-only directory + contexts now release their detach blocker; all file contexts (including mapped + files) and mutation-capable directory contexts remain blockers. Three focused + bookkeeping tests pass. The native regression now holds a browsing directory + across detach. It is now executed and passes in `native_windows_mount` when + `SHELLCANVAS_NATIVE_WINDOWS_TEST=1` and + `SHELLCANVAS_SKIP_PRIVILEGED_VOLUME_FLUSH=1` are set. Updated bridge binary + built 2026-09-11 01:49:46 local; the old isolated helper was stopped only + verifying its profile, W: target, and empty directory, to recover the stuck + test mount. +- WinFsp is installed on the development PC and its launcher is running. Local + native acceptance passes, including I/O, mapped files, metadata, native copy, + injected failures, busy-detach refusal, ordinary detach and pipe-loss cleanup. + This account cannot open a volume for privileged volume-wide flushing; the + explicit `SHELLCANVAS_SKIP_PRIVILEGED_VOLUME_FLUSH=1` test switch omits only + that subtest. Per-file flush remains exercised and the CI default still runs + the privileged check. No CI was triggered for this verification. +- A separate `dev.shellcanvas.driveprobe` profile exercised the actual Windows + file chooser (including cancellation), hash review, bridge installation, + attachment of a disposable SFTP folder on an authorized test host as W:, and + Open folder in Explorer. Native file creation, directory creation and Unicode-file rename + succeeded. Notepad opened and saved the remote text file; an independent SSH + read confirmed the exact saved text. The normal app profile was not changed. + Detach with open handles was refused with a visible explanation. +- The live quit test exposed a Tauri interaction: JavaScript `onCloseRequested` + destroys the window unless its callback prevents default, even if Rust blocks + close. The frontend now always prevents this automatic destruction and asks + the backend to close. The final backend check is serialized with attachment + reservation and marks closing before destruction so waiting attachments cannot + start afterwards. The subsequent attach/detach/quit checklist was confirmed + successful by the user; no further automated UI retest was run. +- Windows attachment selection and preflight now also reserve remembered, + disconnected network-drive letters via `WNetGetConnectionW`; active mount + checks alone omitted this PC's X: and Y:. The standalone bridge and its native + harness apply the same check. Mount cleanup deliberately still checks active + mounts, so a remembered network connection is not mistaken for a live bridge. + The updated bridge native suite passes locally (10.96 seconds). - Unconfirmed SFTP CLOSE now retains cleanup ownership in a guard until the server acknowledges success. Caller cancellation, rejection, timeout, or a dropped cleanup task retires the mount's dedicated channel, releasing remote @@ -282,9 +328,9 @@ Include the dependencies' licensing/distribution limitations in that app. use isolated temporary profiles; no bridge was installed into the user's profile. Three installer tests pass, including changed-review rejection preserving the previous installation. Frontend production build, desktop clippy and 640px - preview overflow checks pass. Native chooser/approval still needs end-to-end use. - Native chooser/approval and an actual desktop-launched Windows mapping remain - unverified; the implementation is a development preview. + preview overflow checks pass. Native chooser/approval and a desktop-launched + Windows SFTP mapping subsequently passed in the isolated profile above; + the implementation remains a development preview until the other gates close. - Files now offers **Attach to this computer…** for folders/current directories, and an Attach action beside mounted volume locations. The dialog checks optional provider support and installation, defaults to read-only, offers a Windows drive @@ -373,10 +419,11 @@ Include the dependencies' licensing/distribution limitations in that app. disposable FUSE test rather than changing the server's installed toolchain. - Verified the official WinFsp 2.1.25156 MSI signature (Navimatics). Installation failed with Windows Installer 1925 / exit 1603: administrator privileges needed. - No successful driver installation on this PC has been established. CI now + The user subsequently installed WinFsp successfully; local acceptance is recorded + above. CI also installs the official runtime on its disposable Windows runner, checking the - pinned MSI SHA-256 and Authenticode signer before native tests. The local UAC question - is pending. This is an OS privilege issue, not an automatic approval rejection. + pinned MSI SHA-256 and Authenticode signer before native tests. The earlier + installer failure was an OS privilege issue, not an automatic approval rejection. Installer/log are under `.local/bridge-tools`. Workspace-only libclang 18.1.1 is available at `.local/bridge-tools/python/clang/native` for Windows builds. @@ -426,10 +473,11 @@ driver, remote account, normal app profile, or CI run was used by these checks. ## Remaining completion gates -1. Native end-to-end installation verification and an actual desktop-created - mapping, including chooser cancel, missing driver and changed executable. - The manager/UI/source leases are implemented; native user-flow verification - remains, including disconnect/source replacement/quit with busy local files. +1. Native chooser cancel, installation, desktop-created SFTP mapping, Explorer + launch, Notepad save and busy-detach refusal now pass on this PC. Remaining: + changed-executable/source-retirement and source-replacement user flows, plus + full failure recovery. The corrected ordinary detach/quit flow has subsequent + user-reported end-to-end acceptance. 2. Open folder and explicit Retry cleanup controls are implemented. Retry retains the original helper/job ownership and connection reservation. After process shutdown, native OS mount-table checks must confirm the location is unmounted; @@ -454,10 +502,10 @@ driver, remote account, normal app profile, or CI run was used by these checks. chooser/source-retirement race remains to be exercised without touching the normal user profile. 4. Native Windows file API, replacement-save, capacity, basic error and busy-detach - checks now pass in disposable WinFsp CI. Remaining: desktop-created SFTP mapping, - Explorer/ordinary editor acceptance (native directory rename/open-handle - semantics are now verified above), - disconnect behavior and failure recovery. Local driver setup is still pending. + checks pass in disposable WinFsp CI and locally. Local driver setup and the + desktop-created SFTP/Explorer/Notepad path are now verified above. Remaining: + full desktop failure recovery. The corrected Windows lifecycle checklist has + user-reported end-to-end acceptance; wider failure matrices remain unverified. 5. Native Linux FUSE file operations, directory rewind/rename, truncate, flush, mapped files, busy detach and ordinary unmount now pass as root and as an unprivileged user on Linux 6.8. Pipe loss with an open file also passes in both @@ -467,6 +515,8 @@ driver, remote account, normal app profile, or CI run was used by these checks. other supported runtimes. Do not infer those from one kernel/runtime. 6. Verify modern macOS compilation and native runtime as available. fuser's kernel/libfuse backend is implemented; FSKit operation is not established. + On this Windows machine, modern macOS runtime verification cannot be + executed yet, so this gate remains unverified here. Do not claim an OS version/runtime works solely because Linux compiled. 7. Resolve currently documented limits before calling the release ready: Windows cleanup-time deletion warnings and busy-detach control are implemented diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d8effeb..c4afa59 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,7 +37,7 @@ rand = "0.10" [target.'cfg(windows)'.dependencies] windows-core = "0.61.2" -windows = { version = "0.61.3", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Memory", "Win32_System_DataExchange", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } +windows = { version = "0.61.3", features = ["Win32_NetworkManagement_WNet", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Memory", "Win32_System_DataExchange", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } [target.'cfg(target_os = "macos")'.dependencies] libc = "0.2" diff --git a/src-tauri/src/drive_mappings.rs b/src-tauri/src/drive_mappings.rs index 3b29703..65bd87b 100644 --- a/src-tauri/src/drive_mappings.rs +++ b/src-tauri/src/drive_mappings.rs @@ -357,9 +357,12 @@ pub async fn attach_drive( // Reserve while holding the registry lock: disconnect/replacement use the same // order and cannot retire this source between validation and grant ownership. let _transition = state.mount_transition.lock().await; + if *_transition { + return Err("ShellCanvas is closing; no new drive can be attached.".into()); + } // Read the local OS table before reserving, without probing filesystem contents. - if crate::local_mounts::occupied(&target)? { - return Err("That local location is already mounted. Choose another location.".into()); + if crate::local_mounts::reserved_for_attachment(&target)? { + return Err("That local location is mounted or reserved by a network drive. Choose another location.".into()); } let registry = state.registry.lock().await; let session = registry diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b448832..4227351 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -51,7 +51,9 @@ use workspace_services::WorkspaceServices as ActiveSession; #[derive(Default)] struct DesktopState { mappings: drive_mappings::Mappings, - mount_transition: Mutex<()>, + // True once final window destruction has been requested; attachments waiting + // for this lock must not start after the UI has gone away. + mount_transition: Mutex, directories: directories::DirectoryReaders, registry: Arc>>, next_id: AtomicU64, @@ -730,6 +732,27 @@ async fn close_terminal( Ok(()) } +/// The frontend always prevents Tauri's automatic destroy. Serialize the final +/// close with attachment reservation so a newly starting mount cannot lose its UI. +#[tauri::command] +async fn request_app_close( + window: tauri::Window, + state: State<'_, DesktopState>, +) -> Result<(), String> { + let mut closing = state.mount_transition.lock().await; + if state.mappings.has_running() { + use tauri::Emitter; + let _ = window.emit("drive-mappings-close-blocked", ()); + return Err("Detach local drives in Settings → Files before quitting ShellCanvas.".into()); + } + *closing = true; + if let Err(error) = window.destroy() { + *closing = false; + return Err(error.to_string()); + } + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -794,6 +817,7 @@ pub fn run() { } let handler: fn(tauri::ipc::Invoke) -> bool = tauri::generate_handler![ client_platform, + request_app_close, drive_mappings::cancel_drive_startup, drive_bridge_install::drive_bridge_installation, drive_bridge_install::review_drive_bridge, diff --git a/src-tauri/src/local_mounts.rs b/src-tauri/src/local_mounts.rs index dccc27e..a821bc1 100644 --- a/src-tauri/src/local_mounts.rs +++ b/src-tauri/src/local_mounts.rs @@ -9,11 +9,58 @@ pub(crate) fn available_drive_letters() -> Result, String> { if mask == 0 { return Err(std::io::Error::last_os_error().to_string()); } - Ok((b'D'..=b'Z') - .rev() - .filter(|letter| mask & (1 << (letter - b'A')) == 0) - .map(|letter| format!("{}:", char::from(letter))) - .collect()) + let mut available = Vec::new(); + for letter in (b'D'..=b'Z').rev() { + let target = format!("{}:", char::from(letter)); + if mask & (1 << (letter - b'A')) == 0 && !network_drive_reserved(&target)? { + available.push(target); + } + } + Ok(available) +} + +#[cfg(windows)] +fn network_drive_reserved(target: &str) -> Result { + use windows::{core::HSTRING, Win32::NetworkManagement::WNet::WNetGetConnectionW}; + let mut remote = [0u16; 256]; + let mut length = remote.len() as u32; + let status = unsafe { + WNetGetConnectionW( + &HSTRING::from(target), + Some(windows::core::PWSTR(remote.as_mut_ptr())), + &mut length, + ) + }; + network_connection_reserved(status.0) +} + +#[cfg(windows)] +fn network_connection_reserved(status: u32) -> Result { + use windows::Win32::Foundation::{ + ERROR_CONNECTION_UNAVAIL, ERROR_MORE_DATA, ERROR_NOT_CONNECTED, NO_ERROR, + }; + match status { + s if s == NO_ERROR.0 || s == ERROR_MORE_DATA.0 || s == ERROR_CONNECTION_UNAVAIL.0 => { + Ok(true) + } + s if s == ERROR_NOT_CONNECTED.0 => Ok(false), + other => Err(format!( + "Unable to check reserved network drives: {}", + std::io::Error::from_raw_os_error(other as i32) + )), + } +} + +/// Reservations include remembered disconnected network drives. Cleanup checks +/// deliberately use occupied() instead: a remembered drive is not our live mount. +pub(crate) fn reserved_for_attachment(target: &Path) -> Result { + if occupied(target)? { + return Ok(true); + } + #[cfg(windows)] + return network_drive_reserved(target.to_str().ok_or("Invalid local drive")?); + #[cfg(not(windows))] + Ok(false) } #[cfg(windows)] @@ -127,6 +174,24 @@ pub(crate) fn occupied(_: &Path) -> Result { #[cfg(test)] mod tests { use super::*; + #[cfg(windows)] + #[test] + fn disconnected_network_connections_still_reserve_the_letter() { + use windows::Win32::Foundation::*; + for status in [NO_ERROR, ERROR_MORE_DATA, ERROR_CONNECTION_UNAVAIL] { + assert_eq!(network_connection_reserved(status.0), Ok(true)); + } + assert_eq!( + network_connection_reserved(ERROR_NOT_CONNECTED.0), + Ok(false) + ); + assert!(network_connection_reserved(ERROR_ACCESS_DENIED.0).is_err()); + let available = available_drive_letters().expect("Read local drive reservations"); + for letter in &available { + assert!(!reserved_for_attachment(Path::new(letter)).unwrap()); + } + println!("Available attachment letters: {available:?}"); + } #[test] fn native_system_volume_is_present_without_filesystem_io() { #[cfg(windows)] diff --git a/src/App.tsx b/src/App.tsx index c016eb4..5cd31eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -154,12 +154,21 @@ export default function App({ void import("@tauri-apps/api/window") .then(async ({ getCurrentWindow }) => { const stop = await getCurrentWindow().onCloseRequested((event) => { + // Tauri destroys the window after this callback unless we prevent it, + // even when Rust has independently rejected the close request. + event.preventDefault(); if ( !closeAllowed.current && (closeState.current.hasUnsaved || closeState.current.hasBusy) ) { - event.preventDefault(); setCloseApp(true); + } else { + void import("@tauri-apps/api/core") + .then(({ invoke }) => invoke("request_app_close")) + .catch((error) => { + closeAllowed.current = false; + setToast(String(error)); + }); } }); if (disposed) stop(); From 398631a5de4084d530d713b8cccfbaa5a3ea20c0 Mon Sep 17 00:00:00 2001 From: Vanyo Vanev Date: Fri, 11 Sep 2026 21:11:54 +0300 Subject: [PATCH 2/4] Give the built-in Canvas theme a neutral palette Canvas 1.1.0 replaces the green-tinted surfaces with neutral white and charcoal, blue accents and a blue/indigo wallpaper in both light and dark, modelled on macOS and Windows 11 defaults. Its dock icons use neutral chrome with distinct app colors; custom themes keep their own palette. The exported Canvas Study example and the preview terminal follow suit. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + docs/themes.md | 7 +- .../canvas-study/shellcanvas.theme.json | 94 +++++++++--------- src/preview.ts | 4 +- src/themes/model.test.ts | 2 +- src/themes/model.ts | 96 +++++++++---------- src/themes/theme.css | 33 ++++++- 7 files changed, 133 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a734300..1022ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ShellCanvas follows semantic versioning for published SDK packages. Desktop rele - Fixed: installed apps opened in a workspace without an SSH target (connection adapters, the preview) now finish starting. Their environment event carried an undefined `host.target`, which the event journal rejected; the field is now omitted. - Fixed: closing ShellCanvas while a local drive is attached no longer hides the window and leaves ShellCanvas and the Drive Bridge running in the background. Quitting is refused with a message until drives are detached, and no drive can start attaching once the window is closing. - Fixed (Windows): drive letters held by remembered but disconnected network drives are no longer offered or accepted for drive attachment. +- Changed: the built-in Canvas theme (1.1.0) uses neutral white and charcoal surfaces with blue accents in both light and dark, replacing the green-tinted palette. Custom themes are unchanged. ## 0.1.1 diff --git a/docs/themes.md b/docs/themes.md index 132632d..57acced 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -1,9 +1,10 @@ # Themes and appearance Open **Desktop settings → Desktop**. Choose a theme and **Light**, **Dark**, or -**Follow system**. Canvas, the built-in theme, has a pale alpine daytime palette -and a deep blue-green evening palette. Changes apply to open windows, menus, -shared dialogs, the editor and the terminal without reconnecting hosts. +**Follow system**. Canvas, the built-in theme, uses neutral white and charcoal +surfaces with blue accents, inspired by macOS and Windows 11. Changes apply to +open windows, menus, shared dialogs, the editor and the terminal without +reconnecting hosts. The same page controls the wallpaper, interface scale (80–150%), top toolbar height and dock icon size. Scaling changes text, controls and spacing through diff --git a/examples/themes/canvas-study/shellcanvas.theme.json b/examples/themes/canvas-study/shellcanvas.theme.json index 34602c7..1a30490 100644 --- a/examples/themes/canvas-study/shellcanvas.theme.json +++ b/examples/themes/canvas-study/shellcanvas.theme.json @@ -2,38 +2,38 @@ "format": 1, "kind": "shellcanvas-theme", "id": "example.canvas-study", - "version": "1.0.0", + "version": "1.1.0", "name": "Canvas Study", "author": "Your name", "description": "A starting point for your own light and dark desktop theme.", "variants": { "dark": { "colors": { - "base": "#122330", - "surface": "#1b2b36", - "raised": "#263943", - "inset": "#15242f", - "text": "#e0e9ea", - "muted": "#a1b8c2", - "accent": "#a3d9c4", - "onAccent": "#173a30", - "border": "#405660", - "hover": "#2b414b", - "selection": "#35564f", + "base": "#19191c", + "surface": "#242427", + "raised": "#303034", + "inset": "#1d1d20", + "text": "#f2f2f5", + "muted": "#b0b0ba", + "accent": "#7ab8ff", + "onAccent": "#102745", + "border": "#48484f", + "hover": "#39393f", + "selection": "#293f5b", "danger": "#f1a7a7", - "dangerSoft": "#452f36", - "warning": "#e5c68a", - "warningSoft": "#3f3a30", - "success": "#a3d9b8", - "shadow": "#07121b", - "scrim": "#07121b", - "terminal": "#111d27", - "terminalText": "#d4e2e7", - "sky": "#193e50", - "glow": "#8cad9f", - "far": "#54757b", - "mid": "#304f5d", - "near": "#18333f" + "dangerSoft": "#462d33", + "warning": "#f2c572", + "warningSoft": "#413527", + "success": "#91d6a3", + "shadow": "#08080d", + "scrim": "#111118", + "terminal": "#161619", + "terminalText": "#e5e5ea", + "sky": "#102655", + "glow": "#536dd4", + "far": "#455b96", + "mid": "#293c72", + "near": "#172345" }, "windowRadius": 14, "windowHeader": 43, @@ -45,31 +45,31 @@ }, "light": { "colors": { - "base": "#e9eeeb", - "surface": "#f8faf7", - "raised": "#edf2ee", - "inset": "#e5ece7", - "text": "#223b3c", - "muted": "#516b6c", - "accent": "#296e60", + "base": "#f0f0f3", + "surface": "#fafafa", + "raised": "#f0f0f3", + "inset": "#e8e8ed", + "text": "#202024", + "muted": "#585861", + "accent": "#005fb8", "onAccent": "#ffffff", - "border": "#c0d0c8", - "hover": "#e1eae3", - "selection": "#c8e1d4", + "border": "#c9c9d1", + "hover": "#e2e2e8", + "selection": "#d6e7fb", "danger": "#a02f3d", "dangerSoft": "#f8e6e5", - "warning": "#795916", - "warningSoft": "#f3ead5", - "success": "#2e7451", - "shadow": "#3c5854", - "scrim": "#304d47", - "terminal": "#f3f7f1", - "terminalText": "#243c36", - "sky": "#d4e4df", - "glow": "#fff0cb", - "far": "#9fbdb8", - "mid": "#799f9b", - "near": "#527e7c" + "warning": "#805400", + "warningSoft": "#fff1d6", + "success": "#26733b", + "shadow": "#252535", + "scrim": "#242432", + "terminal": "#ffffff", + "terminalText": "#242429", + "sky": "#dde8ff", + "glow": "#f4edff", + "far": "#a6b7e2", + "mid": "#748dc3", + "near": "#3b558c" }, "windowRadius": 14, "windowHeader": 43, diff --git a/src/preview.ts b/src/preview.ts index 8f2dc30..f2ebf82 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -134,14 +134,14 @@ export const previewServices: HostServices = { data: Array.from(new TextEncoder().encode(text)), }); output( - "\x1b[38;2;115;205;173m ShellCanvas\x1b[0m / design preview\r\n\r\n Your remote workspace, one connection away.\r\n \x1b[38;2;137;153;166mSample terminal • no commands are executed\x1b[0m\r\n\r\n\x1b[38;2;115;205;173mdemo@atlas\x1b[0m \x1b[38;2;153;183;210m~\x1b[0m $ ", + "\x1b[34m ShellCanvas\x1b[0m / design preview\r\n\r\n Your remote workspace, one connection away.\r\n \x1b[38;2;137;153;166mSample terminal • no commands are executed\x1b[0m\r\n\r\n\x1b[34mdemo@atlas\x1b[0m \x1b[38;2;153;183;210m~\x1b[0m $ ", ); return { resizable: true, write: async (text) => { if (typeof text === "string" ? text.includes("\r") : text.includes(13)) output( - "\r\nPreview only. Connect in the native app to run commands.\r\n\x1b[38;2;115;205;173mdemo@atlas\x1b[0m ~ $ ", + "\r\nPreview only. Connect in the native app to run commands.\r\n\x1b[34mdemo@atlas\x1b[0m ~ $ ", ); }, resize: async () => {}, diff --git a/src/themes/model.test.ts b/src/themes/model.test.ts index d633771..2eb8fb4 100644 --- a/src/themes/model.test.ts +++ b/src/themes/model.test.ts @@ -137,7 +137,7 @@ describe("installed theme collection", () => { createThemeStore(() => storage) .getSnapshot() .themes.map((theme) => theme.version), - ).toEqual(["1.0.0", "1.1.0"]); + ).toEqual([canvasTheme.version, "1.1.0"]); expect(() => store.install(canvasTheme)).toThrow("cannot be replaced"); expect(() => store.remove(canvasTheme.id)).toThrow("built in"); store.remove("studio.paper"); diff --git a/src/themes/model.ts b/src/themes/model.ts index b555f57..c4578a0 100644 --- a/src/themes/model.ts +++ b/src/themes/model.ts @@ -159,39 +159,39 @@ export const canvasTheme: CanvasTheme = { format: 1, kind: "shellcanvas-theme", id: "org.shellcanvas.canvas", - version: "1.0.0", + version: "1.1.0", name: "Canvas", author: "ShellCanvas", description: - "Quiet alpine mornings. Deep, restful evenings. One workspace, in two lights.", + "Clean white. Quiet charcoal. A familiar desktop, with a clear blue accent.", variants: { dark: { colors: { - base: "#122330", - surface: "#1b2b36", - raised: "#263943", - inset: "#15242f", - text: "#e0e9ea", - muted: "#a1b8c2", - accent: "#a3d9c4", - onAccent: "#173a30", - border: "#405660", - hover: "#2b414b", - selection: "#35564f", + base: "#19191c", + surface: "#242427", + raised: "#303034", + inset: "#1d1d20", + text: "#f2f2f5", + muted: "#b0b0ba", + accent: "#7ab8ff", + onAccent: "#102745", + border: "#48484f", + hover: "#39393f", + selection: "#293f5b", danger: "#f1a7a7", - dangerSoft: "#452f36", - warning: "#e5c68a", - warningSoft: "#3f3a30", - success: "#a3d9b8", - shadow: "#07121b", - scrim: "#07121b", - terminal: "#111d27", - terminalText: "#d4e2e7", - sky: "#193e50", - glow: "#8cad9f", - far: "#54757b", - mid: "#304f5d", - near: "#18333f", + dangerSoft: "#462d33", + warning: "#f2c572", + warningSoft: "#413527", + success: "#91d6a3", + shadow: "#08080d", + scrim: "#111118", + terminal: "#161619", + terminalText: "#e5e5ea", + sky: "#102655", + glow: "#536dd4", + far: "#455b96", + mid: "#293c72", + near: "#172345", }, windowRadius: 14, windowHeader: 43, @@ -203,31 +203,31 @@ export const canvasTheme: CanvasTheme = { }, light: { colors: { - base: "#e9eeeb", - surface: "#f8faf7", - raised: "#edf2ee", - inset: "#e5ece7", - text: "#223b3c", - muted: "#516b6c", - accent: "#296e60", + base: "#f0f0f3", + surface: "#fafafa", + raised: "#f0f0f3", + inset: "#e8e8ed", + text: "#202024", + muted: "#585861", + accent: "#005fb8", onAccent: "#ffffff", - border: "#c0d0c8", - hover: "#e1eae3", - selection: "#c8e1d4", + border: "#c9c9d1", + hover: "#e2e2e8", + selection: "#d6e7fb", danger: "#a02f3d", dangerSoft: "#f8e6e5", - warning: "#795916", - warningSoft: "#f3ead5", - success: "#2e7451", - shadow: "#3c5854", - scrim: "#304d47", - terminal: "#f3f7f1", - terminalText: "#243c36", - sky: "#d4e4df", - glow: "#fff0cb", - far: "#9fbdb8", - mid: "#799f9b", - near: "#527e7c", + warning: "#805400", + warningSoft: "#fff1d6", + success: "#26733b", + shadow: "#252535", + scrim: "#242432", + terminal: "#ffffff", + terminalText: "#242429", + sky: "#dde8ff", + glow: "#f4edff", + far: "#a6b7e2", + mid: "#748dc3", + near: "#3b558c", }, windowRadius: 14, windowHeader: 43, diff --git a/src/themes/theme.css b/src/themes/theme.css index 4b6b746..254d792 100644 --- a/src/themes/theme.css +++ b/src/themes/theme.css @@ -90,15 +90,42 @@ :root[data-theme] .bottom-status, :root[data-theme] .desktop-caption, :root[data-theme] .disconnect-button { - color: #e0efec; - text-shadow: 0 1px 3px #122330; + color: #f2f2f7; + text-shadow: 0 1px 3px #171725; } :root[data-theme] .bottom-status svg { - color: #a8d8c0; + color: var(--sc-accent); } :root[data-theme] .dock .running::after { background: var(--sc-accent); } +/* Canvas's default chrome is neutral. App identity colors stay distinct, while + custom themes retain their own palette and the existing icon treatment. */ +:root[data-theme="org.shellcanvas.canvas"] .app-empty { + background: transparent; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon { + background: linear-gradient(145deg, #656873, #383b45); + color: #f5f5f8; + border-color: #ffffff30; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.launcher-icon, +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.connections { + background: linear-gradient(145deg, #70b3ff, #266ace); + color: #ffffff; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.files { + background: linear-gradient(145deg, #ffe39a, #eeb54a); + color: #694711; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.editor { + background: linear-gradient(145deg, #d3c9fa, #9c8acb); + color: #3b305e; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.settings { + background: linear-gradient(145deg, #e1e2e9, #a8aab7); + color: #353641; +} :root[data-custom-wallpaper="true"] .wallpaper-custom .landscape { background-color: var(--sc-base); background-image: var(--sc-wallpaper-image); From 5787504db292595779cb853f9ff95bf56a217487 Mon Sep 17 00:00:00 2001 From: TechArtDev Date: Fri, 11 Sep 2026 23:47:48 +0300 Subject: [PATCH 3/4] Add check for closing state in lib.rs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src-tauri/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4227351..94936b2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -740,6 +740,9 @@ async fn request_app_close( state: State<'_, DesktopState>, ) -> Result<(), String> { let mut closing = state.mount_transition.lock().await; + if *closing { + return Ok(()); + } if state.mappings.has_running() { use tauri::Emitter; let _ = window.emit("drive-mappings-close-blocked", ()); From 87c391beb84f292391db131f377799dee6084ff5 Mon Sep 17 00:00:00 2001 From: TechArtDev Date: Fri, 11 Sep 2026 23:48:06 +0300 Subject: [PATCH 4/4] Update error message for reserved local location Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src-tauri/src/drive_mappings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/drive_mappings.rs b/src-tauri/src/drive_mappings.rs index 65bd87b..95213e1 100644 --- a/src-tauri/src/drive_mappings.rs +++ b/src-tauri/src/drive_mappings.rs @@ -362,7 +362,7 @@ pub async fn attach_drive( } // Read the local OS table before reserving, without probing filesystem contents. if crate::local_mounts::reserved_for_attachment(&target)? { - return Err("That local location is mounted or reserved by a network drive. Choose another location.".into()); + return Err("That local location is already mounted or reserved. Choose another location.".into()); } let registry = state.registry.lock().await; let session = registry