From daf2cf31ce143b1376e8f3ca970ac43c0b33eefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Bene=C5=A1?= Date: Wed, 14 Oct 2026 15:54:55 +0000 Subject: [PATCH] LLeMbas's own window, the leaf icon, one name for the app - The window has no system decoration: a local title bar webview (agent state, a native menu with the tray's items, window buttons) above the content webview, which stays remote with no IPC. Laid out on resize, with a frame to resize by unless maximized. - Icons from the transparent pixel leaf, not the server's tiled icon. - productName llembas-desktop: package, .desktop file and the window's app id now match (KWin showed a generic icon); the visible name stays LLeMbas through a desktop template. Replaces the 0.1.0 package l-le-mbas. - The bundled CLI is a resource in the app's own directory, not /usr/bin/lembas. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + CHANGELOG.md | 16 ++ README.md | 16 +- scripts/fetch-cli.sh | 11 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 4 +- src-tauri/capabilities/default.json | 12 +- src-tauri/icons/128x128.png | Bin 3095 -> 827 bytes src-tauri/icons/128x128@2x.png | Bin 4508 -> 1317 bytes src-tauri/icons/32x32.png | Bin 1488 -> 515 bytes src-tauri/icons/icon.ico | Bin 103971 -> 100780 bytes src-tauri/icons/icon.png | Bin 3347 -> 2888 bytes src-tauri/llembas-desktop.desktop | 12 ++ src-tauri/src/cli.rs | 34 ++-- src-tauri/src/lib.rs | 239 ++++++++++++++++++++++------ src-tauri/tauri.conf.json | 22 ++- ui/app.css | 53 +----- ui/index.html | 1 + ui/titlebar.css | 83 ++++++++++ ui/titlebar.html | 36 +++++ ui/titlebar.js | 50 ++++++ ui/tokens.css | 54 +++++++ 22 files changed, 514 insertions(+), 132 deletions(-) create mode 100644 src-tauri/llembas-desktop.desktop create mode 100644 ui/titlebar.css create mode 100644 ui/titlebar.html create mode 100644 ui/titlebar.js create mode 100644 ui/tokens.css diff --git a/.gitignore b/.gitignore index 62d7b33..9c620fe 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /src-tauri/target/ /src-tauri/gen/ # The bundled LLeMbas CLI: fetched and verified by scripts/fetch-cli.sh, never committed +/src-tauri/binaries/lembas /src-tauri/binaries/lembas-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f4d3f9..67de7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes to LLeMbas Desktop. The format follows ## [Unreleased] +### Added + +- The window is LLeMbas's own: a title bar in the LLeMbas look with the server's address, the + agent's state, a menu (the tray's items, plus Reload) and the window buttons. Closing still keeps + the app in the tray. The server's pages stay in a view of their own, with no access to the app. + +### Fixed + +- The app icon is the leaf alone, on a transparent background, instead of a dark square tile. +- The window shows the app's icon instead of a generic one (the window, the menu entry and the + package now share one name, `llembas-desktop`). +- The package is called `llembas-desktop` (it was `l-le-mbas`) and replaces the old one. +- The bundled LLeMbas CLI is installed in the app's own directory instead of as `/usr/bin/lembas`, + where it could hide, or be hidden by, a `lembas` installed by hand. +- `SHA256SUMS` on the GitHub release no longer lists itself. + ## [0.1.0] — 2026-10-14 The first alpha, for Linux. It carries LLeMbas CLI 1.1.0. diff --git a/README.md b/README.md index 0014364..277d204 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,9 @@ Your [LLeMbas](https://llembas.eu) server in a window of its own, with the agent - **The agent, built in.** LLeMbas Desktop carries the [LLeMbas CLI](https://github.com/LLeMbas/LLeMbas-CLI) and runs it in the background as this computer's device, so agent chats started on the server can work in your projects here, without a terminal. -- **In the tray**: close the window and the agent keeps running; the tray menu starts and stops it, - links the computer, and changes the server. +- **Its own window**: a LLeMbas title bar with the agent's state and a menu (start and stop the + agent, link the computer, reload, change server, quit). Close the window and the agent keeps + running in the tray, which has the same menu. > **Alpha.** Linux first (`.deb` and AppImage, x86-64). Windows and macOS later. @@ -19,12 +20,12 @@ Download the `.deb` or the AppImage from the `SHA256SUMS` in the same release. ```sh -sudo apt install ./LLeMbas_*_amd64.deb # Debian, Ubuntu -chmod +x LLeMbas_*.AppImage && ./LLeMbas_*.AppImage +sudo apt install ./llembas-desktop_*_amd64.deb # Debian, Ubuntu (replaces 0.1.0's l-le-mbas) +chmod +x llembas-desktop_*.AppImage && ./llembas-desktop_*.AppImage ``` -On GNOME the tray needs the *AppIndicator* extension; without it, closing the window hides the -app, and starting it again brings the window back. +On GNOME the tray needs the *AppIndicator* extension; without it, the title bar's menu has +everything, and starting the app again brings a hidden window back. ## Use @@ -33,7 +34,8 @@ app, and starting it again brings the window back. 3. The agent starts. In the server's new-chat screen, choose this computer for an agent chat. The agent is the LLeMbas CLI's `lembas service`, using the CLI's own configuration -(`~/.config/lembas`). If you also installed the CLI and run `lembas service` yourself, the app sees +(`~/.config/lembas`). The app's copy of the CLI lives in its own directory +(`/usr/lib/llembas-desktop/`), not on your `PATH`. If you also installed the CLI and run `lembas service` yourself, the app sees it and does not start a second one: they are the same device. ## Build diff --git a/scripts/fetch-cli.sh b/scripts/fetch-cli.sh index cbd2f8f..a0dbd02 100755 --- a/scripts/fetch-cli.sh +++ b/scripts/fetch-cli.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Put the LLeMbas CLI that LLeMbas Desktop carries where Tauri's bundler wants it: -# src-tauri/binaries/lembas- +# Put the LLeMbas CLI that LLeMbas Desktop carries where the bundle takes it from: +# src-tauri/binaries/lembas (a resource: installed in the app's own directory, never on PATH) # # scripts/fetch-cli.sh [target-triple] (default: this machine's, from rustc) # @@ -39,6 +39,7 @@ ssh-keygen -Y verify -f "$work/allowed" -I lembas-release -n lembas-release \ || { echo "fetch-cli: $ASSET.gz does not match SHA256SUMS" >&2; exit 1; } mkdir -p src-tauri/binaries -gunzip -c "$work/$ASSET.gz" > "src-tauri/binaries/lembas-$TRIPLE" -chmod 755 "src-tauri/binaries/lembas-$TRIPLE" -echo "fetch-cli: LLeMbas CLI $VERSION ($ASSET) -> src-tauri/binaries/lembas-$TRIPLE" +gunzip -c "$work/$ASSET.gz" > src-tauri/binaries/lembas +chmod 755 src-tauri/binaries/lembas +rm -f src-tauri/binaries/lembas-* # where an earlier version of this script put it +echo "fetch-cli: LLeMbas CLI $VERSION ($ASSET) -> src-tauri/binaries/lembas" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bb49beb..3c4b644 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1987,7 +1987,7 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "llembas-desktop" -version = "0.1.0" +version = "0.2.0" dependencies = [ "libc", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e36623d..64fbb71 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "llembas-desktop" -version = "0.1.0" +version = "0.2.0" description = "LLeMbas Desktop: your LLeMbas server in a window, with the agent on this computer" authors = ["Jaroslav Beneš"] license = "MIT" @@ -17,7 +17,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["tray-icon"] } +tauri = { version = "2", features = ["tray-icon", "unstable"] } tauri-plugin-dialog = "2" tauri-plugin-opener = "2" tauri-plugin-single-instance = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 93aae47..8b91ffa 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,7 +1,15 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "The app's own pages (the first-run page). The server's web UI is a remote page and gets no IPC at all: this capability lists no remote URLs.", + "description": "The app's own pages: the title bar and the first-run page. The server's web UI is a remote page in the same window and gets no IPC at all: this capability lists no remote URL. The window permissions are the title bar's: moving, resizing, minimising, maximising and closing (which hides).", "windows": ["main"], - "permissions": ["core:default"] + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "core:window:allow-start-resize-dragging", + "core:window:allow-internal-toggle-maximize", + "core:window:allow-minimize", + "core:window:allow-toggle-maximize", + "core:window:allow-close" + ] } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 644ca6099e777aba9c0752a8669196160e4bec06..a189b0ce3d25ebd2d4d9833dad14d96feed7edd1 100644 GIT binary patch literal 827 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRS3dtTpz6=aiYC!Q828LfC`4LAo zfl1oa#WAGf*4sM^^F#s}+Aca3w6vJN5dI*h!6SD#`2(K{FJJ#b1rD}Wha?3@GYyt$ z0vu8zMfsDx)6>)T7r#j{zx;0c>_0Pe&z}RD-_RwUeg8n*-F?O1&)@p~;r#n}`(^gW zXJ_BHc`f8Hhv5;6fO>-kdv!yZ2SKFdiG%J`T2Uw?-iTeOnaO8s@H-M7Ig!?#AD1`W6kC7HRY#U+Wk1-SJHJ-r?W)WhKE>gTe~DWM4f$R8;m delta 2965 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 diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index 486f60824a54ec3d0dc6b76dccc63ae3fba36aa8..e274c08e7406bcf122f73e1e0b5ef2376e1f0e84 100644 GIT binary patch delta 365 zcmcb>-OMr}teyb`emd_4G1yByeO=jKaq;mRS!VdAO#%u%_H=O!i8!2`vcTxUGtnRZ zJ-gP=w>K~{0D{2Vj7yl@vVK>5y`0Gd0_ykf9$URWeEa;wfEEVstu;K%FYNj!eRqF2 zef@i*_b10Jtt9>^Ht?-@ z`q@#K$>KHBmmSNmJb$sW{z41GK@$h<`9L!kOg+!;%OP-vHQiozK_{5)!+csUFC_s8 zc2vAm3!A@)IjPnz!-?VW)(tz#BVV|N&gc6laNuci*3Xg;Z3i5F)&Px@DfrCwM}1p5 zuk{)y1_q&p)vJv6bR@wK6rZGBwvWFt9Q(Fj?zoJ2@VkjO&MQjgu#^ XDkz|95qf$(4ivDSu6{1-oD!M<2fB{I delta 1396 zcmV-)1&jKF1kekRR)7Bh{{a7>y{D4^000McNliru>Iw`96a-wocc}mX1h7d&K~z}7 z)tAk0R8<(pfA2Z>-kDCPrJc4?N);nwQez|<7sidogbl&eL`lGp5dQ*oLF2-OD;F&M z190QUotUU$hhU=;6Cx#iG^n&71zTFC9XcO(?#DUD#q@*H#(!yxLR`(Wn0sdK?|Gm1 zJ+I`q6r6})twlt5&{~NI00KM=?OAIPVDa-oYi|+7;=kIgR!UG>qpSrb2!dFHDAjn| z#5CUio@c%L#kk-jAe2iLoUwS~fq=6X5rxB{15J<%!2O#RIES?klcZ!@v%E7jNX<)_ ztyfS=aMt1wvVW}^#)rqb`tvn{TyTFD+>KgkMK+fqi4)qo^BnwWkhxr)vGOf)UIynJ zX_De~1{hKQmSH1M82b)Pmh00p?eESlJaYDjLLL~7R{c%m*~v4 z6GnAD8~%bDSEulN54ULx1Wankwq_U{dJR9&1X-Wn{(o-je#&eZ5>{gR^1bZHZKtQD zz<6#BVh{mm49=QOTA&0`mRwtow}%Gl=x8UhG4*Plsq!2X;Viz=?8t5B+4cb< zBF^5t#5duA!>sbAc7J_EsQA@=PAvV(Viks5-wJ6P)S34rPuem=lCw{Z)!2w^4UxmTX%#eL6{Z*5`Tgv>G z*&cS}w$tTzGFH6K$jwnoVVRT^Yb{Z!P8ufYj0bk*`Kp0eEj5g_wDL4*YI$MLZr=M~ zKPphn)aSWTpGC_u_Jov?o1=Vp{(Bq_g~C~f@_#j=7xAw=e|;7J*4C)ZRj5$oTCUp!ZJPiE{rjFIiEl>3unnfj5d;jg~|_Fd=7m022Z}clRxfEQnhuq_}XXxqkntZ*1DByT;s^e&#AZ?{r!CyV-PJQm4wqn z!-O--*rEA+X>LJkiKCb+;Wft01e24~RHVk_*}s^(P()`m&N*yi38$;1^|UGW^`$@w zN-O*pA88SOxjsU%e20ngDY~~6D4w0+w_~GZGFeQVHolt=ma(sM#IJ3EfYOR;v46tA zThFlT-~eA8`kJ^Lfo|Mz(kLNcXaf;Es~b0;bz)zkTiZL`5*DQz%_lbKjK|}9pFp>0 zOxWykywVEj*ttHsh60~J-)-F}`f3cdaPJsiR_3%A~Gn=UD`?26|M2iFDf3uox zRzKYBSm1A?elj=|q=cT6VF4U}Fga8*GCDCeIx{pYFfckWFd8IJcmMzZC3HntbYx+4 zWjbwdWNBu305UK#HZ3tQEigG$GBP?bG&(alD=;uRFfcDb5bYX3905UK#HZ3tQEip1wFgQ9gFgh?aD=;uRFfhwxKF9z7002n`MNUMnLSTXf CWtAfU diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 1a3a49f0f8799b65f16b6d71ddd821d609d7b7fa..049459f321e21175c97b85e4464a792c670e4a11 100644 GIT binary patch literal 100780 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 literal 103971 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 = OnceLock::new(); + +/// Where the bundle put the CLI: the app's resource directory (`/usr/lib/llembas-desktop/` from +/// the .deb, inside the AppImage otherwise) — the app's own copy, never on PATH, so it neither +/// shadows nor is shadowed by a `lembas` installed by hand. Set once at start. +pub fn set_bundled(resource_dir: &Path) { + let _ = BUNDLED.set(resource_dir.join(exe_name())); +} + +fn exe_name() -> &'static str { + if cfg!(windows) { + "lembas.exe" + } else { + "lembas" + } +} + +/// The CLI to run: `LLEMBAS_DESKTOP_CLI` (development), else the bundled one, else one beside +/// the app's executable (`cargo tauri dev`). pub fn binary() -> PathBuf { if let Some(p) = std::env::var_os("LLEMBAS_DESKTOP_CLI") { return PathBuf::from(p); } - let name = if cfg!(windows) { - "lembas.exe" - } else { - "lembas" - }; + if let Some(p) = BUNDLED.get().filter(|p| p.exists()) { + return p.clone(); + } std::env::current_exe() .ok() - .and_then(|e| e.parent().map(|d| d.join(name))) - .unwrap_or_else(|| PathBuf::from(name)) + .and_then(|e| e.parent().map(|d| d.join(exe_name()))) + .unwrap_or_else(|| PathBuf::from(exe_name())) } fn home() -> PathBuf { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3416adc..98e9f89 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,11 @@ //! LLeMbas Desktop: the server's web UI in a window of its own, and the LLeMbas CLI's agent //! running on this computer, so agent chats can work in its projects without a terminal. //! -//! - **One window, `main`.** Before a server is chosen it shows the app's own first-run page -//! (`ui/`); after, the server itself. Closing it hides it; the tray brings it back. +//! - **One window, `main`, drawn by the app**: no system decoration, the app's own title bar (a +//! local page, `ui/titlebar.html`) on top and the content below it — before a server is chosen +//! the first-run page (`ui/index.html`), after it the server itself. Two webviews in one window, +//! so the server's pages never share a document with the app's controls. Closing hides the +//! window; the tray, the title bar's menu or starting the app again bring it back. //! - **The server's pages get no IPC.** They are remote content, and the one capability lists no //! remote URL. Links that leave the server open in the system browser. //! - **The agent is the CLI's**: `lembas login --json` links this computer, and @@ -20,8 +23,11 @@ use std::sync::Mutex; use serde::Serialize; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::TrayIconBuilder; +use tauri::webview::WebviewBuilder; +use tauri::window::{Color, WindowBuilder}; use tauri::{ - AppHandle, Emitter, Manager, RunEvent, Url, WebviewUrl, WebviewWindowBuilder, WindowEvent, Wry, + AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, PhysicalPosition, PhysicalSize, + RunEvent, Url, Webview, WebviewUrl, Window, WindowEvent, Wry, }; use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; use tauri_plugin_opener::OpenerExt; @@ -30,6 +36,13 @@ use agent::{Agent, State as AgentState}; use settings::Settings; const MAIN: &str = "main"; +const TITLEBAR: &str = "titlebar"; +const CONTENT: &str = "content"; +/// The title bar's height, in logical pixels: `--titlebar-h` in `ui/titlebar.css`. +const TITLE_H: f64 = 36.0; +/// The frame left around the webviews while the window is not maximized: the window's own +/// surface, where the pointer resizes it (a webview over the edge would take the events). +const FRAME: f64 = 4.0; struct App { config_dir: PathBuf, @@ -156,35 +169,92 @@ fn local_page() -> Url { Url::parse(base).expect("a fixed URL") } +fn content(handle: &AppHandle) -> Option { + handle.get_webview(CONTENT) +} + +fn main_window(handle: &AppHandle) -> Option { + handle.get_window(MAIN) +} + /// Show the main window on `server`, or on the first-run page; made when it does not exist. fn open_main(handle: &AppHandle, server: Option<&str>) -> Result<(), String> { let target = match server { Some(s) => Some(Url::parse(s).map_err(|e| format!("{s}: {e}"))?), None => None, }; - if let Some(w) = handle.get_webview_window(MAIN) { - w.navigate(target.unwrap_or_else(local_page)) + if let (Some(w), Some(c)) = (main_window(handle), content(handle)) { + c.navigate(target.unwrap_or_else(local_page)) .map_err(|e| e.to_string())?; let _ = w.show(); let _ = w.unminimize(); let _ = w.set_focus(); return Ok(()); } + build_main(handle, target).map_err(|e| e.to_string()) +} + +fn build_main(handle: &AppHandle, target: Option) -> tauri::Result<()> { + let window = WindowBuilder::new(handle, MAIN) + .title("LLeMbas") + .inner_size(1280.0, 840.0) + .min_inner_size(420.0, 560.0) + .decorations(false) + // The frame's colour: the Moria border, as `--border` in the pages. + .background_color(Color(0x26, 0x30, 0x3A, 0xFF)) + .build()?; + let size = window + .inner_size()? + .to_logical::(window.scale_factor()?); + window.add_child( + WebviewBuilder::new(TITLEBAR, WebviewUrl::App("titlebar.html".into())), + LogicalPosition::new(FRAME, FRAME), + LogicalSize::new(size.width - 2.0 * FRAME, TITLE_H), + )?; let url = match target { Some(u) => WebviewUrl::External(u), None => WebviewUrl::App("index.html".into()), }; let nav = handle.clone(); - WebviewWindowBuilder::new(handle, MAIN, url) - .title("LLeMbas") - .inner_size(1280.0, 840.0) - .min_inner_size(420.0, 560.0) - .on_navigation(move |url| allowed(&nav, url)) - .build() - .map_err(|e| e.to_string())?; + window.add_child( + WebviewBuilder::new(CONTENT, url).on_navigation(move |url| allowed(&nav, url)), + LogicalPosition::new(FRAME, FRAME + TITLE_H), + LogicalSize::new( + size.width - 2.0 * FRAME, + (size.height - TITLE_H - 2.0 * FRAME).max(0.0), + ), + )?; Ok(()) } +/// Lay the two webviews out in the window: the title bar on top, the content under it, and the +/// frame around both unless the window is maximized (then there is nothing to resize). +fn layout(window: &Window) { + let handle = window.app_handle(); + let (Some(bar), Some(body)) = (handle.get_webview(TITLEBAR), handle.get_webview(CONTENT)) + else { + return; + }; + let (Ok(size), Ok(scale)) = (window.inner_size(), window.scale_factor()) else { + return; + }; + let frame = if window.is_maximized().unwrap_or(false) || window.is_fullscreen().unwrap_or(false) + { + 0 + } else { + (FRAME * scale).round() as u32 + }; + let title = (TITLE_H * scale).round() as u32; + let width = size.width.saturating_sub(2 * frame); + let _ = bar.set_position(PhysicalPosition::new(frame, frame)); + let _ = bar.set_size(PhysicalSize::new(width, title)); + let _ = body.set_position(PhysicalPosition::new(frame, frame + title)); + let _ = body.set_size(PhysicalSize::new( + width, + size.height.saturating_sub(title + 2 * frame), + )); +} + /// The window may show the app's own pages and the server; anything else opens in the browser. fn allowed(handle: &AppHandle, url: &Url) -> bool { let local = matches!(url.scheme(), "tauri" | "asset") @@ -250,8 +320,10 @@ fn start_link(handle: &AppHandle) -> Result<(), String> { .and_then(|v| v.as_str()) .unwrap_or(""); if let Ok(url) = Url::parse(link) { - if let Some(w) = handle.get_webview_window(MAIN) { - let _ = w.navigate(url); + if let Some(c) = content(&handle) { + let _ = c.navigate(url); + } + if let Some(w) = main_window(&handle) { let _ = w.show(); let _ = w.set_focus(); } @@ -304,7 +376,7 @@ fn status_of(handle: &AppHandle) -> Status { fn show(handle: &AppHandle) { let server = handle.state::().settings().server; - if let Some(w) = handle.get_webview_window(MAIN) { + if let Some(w) = main_window(handle) { let _ = w.show(); let _ = w.unminimize(); let _ = w.set_focus(); @@ -341,35 +413,7 @@ fn build_tray(handle: &AppHandle) -> tauri::Result { )?; let mut tray = TrayIconBuilder::with_id("tray") .tooltip("LLeMbas") - .menu(&menu) - .on_menu_event(|h, ev| match ev.id().as_ref() { - "open" => show(h), - "toggle" => { - let on = !h.state::().agent.wanted(); - if let Err(e) = set_agent_wanted(h, on) { - h.dialog() - .message(e) - .kind(MessageDialogKind::Error) - .show(|_| {}); - } - } - "link" => { - if let Err(e) = start_link(h) { - h.dialog() - .message(e) - .kind(MessageDialogKind::Warning) - .show(|_| {}); - } - } - "server" => { - let _ = open_main(h, None); - } - "quit" => { - h.state::().agent.stop(); - h.exit(0); - } - _ => {} - }); + .menu(&menu); if let Some(icon) = handle.default_window_icon() { tray = tray.icon(icon.clone()); } @@ -377,6 +421,85 @@ fn build_tray(handle: &AppHandle) -> tauri::Result { Ok(TrayItems { status, toggle }) } +/// A menu item chosen, from the tray or from the title bar's menu: the same ids in both. +fn on_menu(h: &AppHandle, id: &str) { + match id { + "open" => show(h), + "toggle" => { + let on = !h.state::().agent.wanted(); + if let Err(e) = set_agent_wanted(h, on) { + h.dialog() + .message(e) + .kind(MessageDialogKind::Error) + .show(|_| {}); + } + } + "link" => { + if let Err(e) = start_link(h) { + h.dialog() + .message(e) + .kind(MessageDialogKind::Warning) + .show(|_| {}); + } + } + "reload" => { + if let Some(c) = content(h) { + if let Ok(url) = c.url() { + let _ = c.navigate(url); + } + } + } + "server" => { + let _ = open_main(h, None); + } + "quit" => { + h.state::().agent.stop(); + h.exit(0); + } + _ => {} + } +} + +/// The title bar's menu: the tray's items, where the window is (a desktop without a tray — +/// GNOME without the AppIndicator extension — has no other way to reach them). +#[tauri::command] +fn titlebar_menu(window: Window) -> Result<(), String> { + let h = window.app_handle(); + let app = h.state::(); + let m = |id: &str, text: &str, on: bool| MenuItem::with_id(h, id, text, on, None::<&str>); + let build = || -> tauri::Result> { + let status = m("status", &app.agent.state().label(), false)?; + let toggle = m( + "toggle", + if app.agent.wanted() { + "Stop the agent" + } else { + "Start the agent" + }, + true, + )?; + let link = m("link", "Link this computer…", true)?; + let reload = m("reload", "Reload", true)?; + let server = m("server", "Change server…", true)?; + let quit = m("quit", "Quit LLeMbas", true)?; + Menu::with_items( + h, + &[ + &status, + &toggle, + &link, + &PredefinedMenuItem::separator(h)?, + &reload, + &server, + &PredefinedMenuItem::separator(h)?, + &quit, + ], + ) + }; + let menu = build().map_err(|e| e.to_string())?; + window.popup_menu(&menu).map_err(|e| e.to_string()) +} + fn on_agent_state(handle: &AppHandle, state: &AgentState) { let app = handle.state::(); if let Some(t) = app.tray.lock().unwrap().as_ref() { @@ -399,11 +522,16 @@ pub fn run() { get_status, set_server, set_agent, - link_computer + link_computer, + titlebar_menu ]) + .on_menu_event(|h, ev| on_menu(h, ev.id().as_ref())) .setup(|app| { let handle = app.handle().clone(); let config_dir = app.path().app_config_dir()?; + if let Ok(dir) = app.path().resource_dir() { + cli::set_bundled(&dir); + } let log = app.path().app_log_dir()?.join("agent.log"); let notify_handle = handle.clone(); let agent = Agent::new(log, move |s| { @@ -428,14 +556,23 @@ pub fn run() { } Ok(()) }) - .on_window_event(|window, event| { + .on_window_event(|window, event| match event { // Closing the window keeps the app (and the agent) in the tray. - if let WindowEvent::CloseRequested { api, .. } = event { - if window.label() == MAIN { - let _ = window.hide(); - api.prevent_close(); - } + WindowEvent::CloseRequested { api, .. } if window.label() == MAIN => { + let _ = window.hide(); + api.prevent_close(); } + WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } + if window.label() == MAIN => + { + layout(window); + let _ = window.emit_to( + TITLEBAR, + "maximized", + window.is_maximized().unwrap_or(false), + ); + } + _ => {} }) .build(tauri::generate_context!()) .expect("LLeMbas Desktop could not start") diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 069ea5a..b0d0688 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "LLeMbas", + "productName": "llembas-desktop", "mainBinaryName": "llembas-desktop", "identifier": "eu.llembas.desktop", "build": { @@ -15,7 +15,10 @@ }, "bundle": { "active": true, - "targets": ["deb", "appimage"], + "targets": [ + "deb", + "appimage" + ], "category": "Productivity", "shortDescription": "Your LLeMbas server in a window, with the agent on this computer", "longDescription": "LLeMbas Desktop opens your LLeMbas server's web UI in a window of its own and runs the LLeMbas CLI agent on this computer, so agent chats can work in your projects here without a terminal.", @@ -30,6 +33,19 @@ "icons/icon.png", "icons/icon.ico" ], - "externalBin": ["binaries/lembas"] + "resources": { + "binaries/lembas": "lembas" + }, + "linux": { + "deb": { + "desktopTemplate": "llembas-desktop.desktop", + "conflicts": [ + "l-le-mbas" + ], + "replaces": [ + "l-le-mbas" + ] + } + } } } diff --git a/ui/app.css b/ui/app.css index c58ab8f..28ccd05 100644 --- a/ui/app.css +++ b/ui/app.css @@ -1,55 +1,4 @@ -/* The first-run page: the LLeMbas look (Moria dark, Shire light), no build step. */ -:root { - --font-ui: ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", Menlo, Consolas, monospace; - --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - --text-sm: 0.8125rem; - --text-base: 0.9375rem; - --text-xl: 1.5rem; - --sp-2: 0.5rem; - --sp-3: 0.75rem; - --sp-4: 1rem; - --sp-6: 1.5rem; - --sp-8: 2rem; - --radius: 2px; - --press: 2px; - --control-h: 2.5rem; - --card-max: 34rem; - - color-scheme: dark; - --bg: #0B0D10; - --surface: #11151A; - --surface-hover: #1D232B; - --border: #26303A; - --border-strong: #5C6976; - --ink: #E2E8EC; - --ink-muted: #A6B2BC; - --accent: #8FD8E8; - --accent-hover: #B4E6F1; - --accent-ink: #061418; - --leaf: #7EE081; - --warning: #F0A840; - --shadow-card: 4px 4px 0 rgba(0, 0, 0, 0.55); -} - -@media (prefers-color-scheme: light) { - :root { - color-scheme: light; - --bg: #F1EAD6; - --surface: #F8F3E5; - --surface-hover: #ECE4CF; - --border: #CBBF9D; - --border-strong: #2A2E22; - --ink: #1F2418; - --ink-muted: #474D3B; - --accent: #285F3C; - --accent-hover: #1F4D30; - --accent-ink: #F8F3E5; - --leaf: #3D6B1C; - --warning: #7E5200; - --shadow-card: 4px 4px 0 var(--border-strong); - } -} - +/* The first-run page. Tokens in tokens.css. */ * { box-sizing: border-box; } html, body { margin: 0; min-height: 100%; } diff --git a/ui/index.html b/ui/index.html index e21ddad..cb23648 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,6 +5,7 @@ LLeMbas + diff --git a/ui/titlebar.css b/ui/titlebar.css new file mode 100644 index 0000000..c1ad606 --- /dev/null +++ b/ui/titlebar.css @@ -0,0 +1,83 @@ +/* The title bar: one row, fixed height, every slot a fixed or shared track so nothing in it + moves when the host name or the agent's state changes length. */ +* { box-sizing: border-box; } + +html, body { + margin: 0; + height: var(--titlebar-h); + overflow: hidden; + background: var(--bg); + color: var(--ink); + user-select: none; + -webkit-user-select: none; + cursor: default; +} + +.bar { + display: grid; + /* leaf · name · host (takes the rest, and is the drag area) · agent · menu · sep · 3 controls */ + grid-template-columns: var(--titlebar-h) auto minmax(0, 1fr) auto var(--titlebar-h) 1px repeat(3, 2.875rem); + align-items: center; + height: var(--titlebar-h); + border-bottom: 1px solid var(--border); + font: 600 var(--text-sm) / 1 var(--font-ui); +} + +.bar__leaf { justify-self: center; width: 18px; height: 18px; image-rendering: pixelated; } +.bar__name b { color: var(--leaf); font-weight: 700; } +.bar__host { + padding: 0 var(--sp-3); + color: var(--ink-muted); + font-weight: 400; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + align-self: stretch; + display: flex; + align-items: center; +} + +button { + height: var(--titlebar-h); + margin: 0; + padding: 0; + font: inherit; + color: var(--ink-muted); + background: transparent; + border: 0; + border-radius: 0; + cursor: pointer; +} +button:hover { color: var(--ink); background: var(--surface-hover); } +button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } + +.bar__agent { + display: grid; + grid-template-columns: 0.5rem auto; + gap: var(--sp-2); + align-items: center; + padding: 0 var(--sp-3); + font-weight: 400; +} +.dot { width: 0.5rem; height: 0.5rem; background: var(--ink-muted); } +.bar__agent[data-state="running"] .dot, +.bar__agent[data-state="elsewhere"] .dot { background: var(--leaf); } +.bar__agent[data-state="starting"] .dot { background: var(--accent); } +.bar__agent[data-state="failed"] .dot, +.bar__agent[data-state="not_linked"] .dot { background: var(--warning); } + +.bar__sep { width: 1px; height: 1.25rem; background: var(--border); } + +.bar__btn { display: grid; place-items: center; } +.bar__btn svg { width: 16px; height: 16px; } +.bar__btn--close:hover { color: #fff; background: var(--danger); } + +#maximize .i-restore { display: none; } +#maximize[data-maximized] .i-max { display: none; } +#maximize[data-maximized] .i-restore { display: block; } + +/* Narrow windows: the host goes first, then the agent's words (its dot stays). */ +@media (max-width: 34rem) { + .bar__host { visibility: hidden; } + #agent-text { display: none; } +} diff --git a/ui/titlebar.html b/ui/titlebar.html new file mode 100644 index 0000000..5923777 --- /dev/null +++ b/ui/titlebar.html @@ -0,0 +1,36 @@ + + + + + +LLeMbas + + + + + +

+ + LLeMbas + + + + + + + + +
+ + diff --git a/ui/titlebar.js b/ui/titlebar.js new file mode 100644 index 0000000..d0deac2 --- /dev/null +++ b/ui/titlebar.js @@ -0,0 +1,50 @@ +// The title bar. Moving and double-click maximising come from `data-tauri-drag-region`; the +// buttons call the window directly; the menu and the agent pill open the app's native menu (the +// tray's items), which can reach past this 36-pixel-high page. +"use strict" + +const { invoke } = window.__TAURI__.core +const { listen } = window.__TAURI__.event +const win = window.__TAURI__.window.getCurrentWindow() + +const $ = (id) => document.getElementById(id) + +const WORDS = { + off: "agent off", + not_linked: "not linked", + starting: "agent starting…", + running: "agent running", + elsewhere: "agent running (service)", + failed: "agent stopped", +} + +function render(s) { + let host = "" + try { + host = s.server ? new URL(s.server).host : "" + } catch {} + $("host").textContent = host + $("host").title = s.server || "" + const state = s.agent.state + $("agent").dataset.state = state + $("agent-text").textContent = WORDS[state] ?? state + $("agent").title = state === "failed" ? `Agent stopped: ${s.agent.detail}` : "Agent" +} + +function maximized(on) { + const b = $("maximize") + b.toggleAttribute("data-maximized", on) + b.setAttribute("aria-label", on ? "Restore" : "Maximise") + b.title = on ? "Restore" : "Maximise" +} + +$("minimize").addEventListener("click", () => win.minimize()) +$("maximize").addEventListener("click", () => win.toggleMaximize()) +$("close").addEventListener("click", () => win.close()) +$("menu").addEventListener("click", () => invoke("titlebar_menu")) +$("agent").addEventListener("click", () => invoke("titlebar_menu")) + +listen("status", (e) => render(e.payload)) +listen("maximized", (e) => maximized(!!e.payload)) +invoke("get_status").then(render) +win.isMaximized().then(maximized) diff --git a/ui/tokens.css b/ui/tokens.css new file mode 100644 index 0000000..9c21d2c --- /dev/null +++ b/ui/tokens.css @@ -0,0 +1,54 @@ +/* LLeMbas Desktop's own pages: one set of tokens (Moria dark, Shire light), as on llembas.eu. */ +:root { + --font-ui: ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", Menlo, Consolas, monospace; + --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --text-sm: 0.8125rem; + --text-base: 0.9375rem; + --text-xl: 1.5rem; + --sp-2: 0.5rem; + --sp-3: 0.75rem; + --sp-4: 1rem; + --sp-6: 1.5rem; + --sp-8: 2rem; + --radius: 2px; + --press: 2px; + --control-h: 2.5rem; + --card-max: 34rem; + + color-scheme: dark; + --bg: #0B0D10; + --surface: #11151A; + --surface-hover: #1D232B; + --border: #26303A; + --border-strong: #5C6976; + --ink: #E2E8EC; + --ink-muted: #A6B2BC; + --accent: #8FD8E8; + --accent-hover: #B4E6F1; + --accent-ink: #061418; + --leaf: #7EE081; + --warning: #F0A840; + --shadow-card: 4px 4px 0 rgba(0, 0, 0, 0.55); + --danger: #E5534B; + --titlebar-h: 36px; +} + +@media (prefers-color-scheme: light) { + :root { + color-scheme: light; + --bg: #F1EAD6; + --surface: #F8F3E5; + --surface-hover: #ECE4CF; + --border: #CBBF9D; + --border-strong: #2A2E22; + --ink: #1F2418; + --ink-muted: #474D3B; + --accent: #285F3C; + --accent-hover: #1F4D30; + --accent-ink: #F8F3E5; + --leaf: #3D6B1C; + --warning: #7E5200; + --shadow-card: 4px 4px 0 var(--border-strong); + --danger: #B3261E; + } +}