diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index c607a07c..4c95c2dd 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -26,6 +26,7 @@ dependencies = [ "tauri-plugin-process", "tauri-plugin-single-instance", "tauri-plugin-updater", + "tauri-plugin-window-state", "tempfile", ] @@ -4099,6 +4100,21 @@ dependencies = [ "zip", ] +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.13.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-runtime" version = "2.11.3" diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 574654bd..008d8d5d 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -24,6 +24,7 @@ tauri-plugin-dialog = "2" tauri-plugin-updater = "2" tauri-plugin-process = "2" tauri-plugin-single-instance = "2" +tauri-plugin-window-state = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" # Terminal-embedded core: own agent PTYs (spawn + read + write + inject) and diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index debf4d49..c92322d1 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -498,6 +498,11 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) + // Restores the main window's size/position/maximized state on + // launch and saves it on move/resize/close — fully automatic, no + // frontend involvement needed (unlike zoom/view-visibility, which + // are app-specific state the frontend also reads/writes). + .plugin(tauri_plugin_window_state::Builder::default().build()) // Built in English first — the frontend doesn't get a chance to // report its actual language until after the webview loads and // i18next resolves it, so set_menu_language rebuilds this shortly diff --git a/app/src/App.css b/app/src/App.css index 18e29b66..262469ff 100644 --- a/app/src/App.css +++ b/app/src/App.css @@ -3,6 +3,11 @@ --panel: #11151f; --panel-2: #161b27; --border: #232a3a; + /* One step brighter than --border, specifically for the pane frame + (koit: --border alone was too faint to read as a visible box at + rest). Its own variable, not a --border change, so the other 27 + places --border is used elsewhere in the UI stay as-is. */ + --pane-border: #3a4660; --fg: #c5c8c6; --muted: #6b7280; --accent: #7aa2f7; @@ -1029,7 +1034,7 @@ body.resizing-row { font-size: 11px; color: var(--muted); background: var(--panel); - border: 1px solid var(--border); + border: 1px solid var(--pane-border); border-bottom: none; border-radius: 6px 6px 0 0; } @@ -1123,7 +1128,11 @@ body.resizing-row { .pane-divider-h::before { content: ""; position: absolute; - background: var(--border); + /* Invisible at rest now that the gap itself is a full terminal cell + (koit) — a static hairline sitting in the middle of that gap read as + a redundant, always-on divider line. Shows only on hover/while + actively dragging (below), same as before. */ + background: transparent; } .pane-divider-v::before { top: 0; @@ -1139,13 +1148,15 @@ body.resizing-row { height: 1px; transform: translateY(-50%); } -.pane-divider-v:hover::before { +.pane-divider-v:hover::before, +.pane-divider-v.pane-divider-dragging::before { left: 0; width: 100%; transform: none; background: var(--accent); } -.pane-divider-h:hover::before { +.pane-divider-h:hover::before, +.pane-divider-h.pane-divider-dragging::before { top: 0; height: 100%; transform: none; @@ -1268,7 +1279,7 @@ body.resizing-row { min-height: 0; width: 100%; height: auto; - border: 1px solid var(--border); + border: 1px solid var(--pane-border); border-top: none; } diff --git a/app/src/App.tsx b/app/src/App.tsx index ec9bb036..f4a59b34 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -18,6 +18,8 @@ import { AgentModal, AppUserModal, ConfirmModal, + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, NewTeamModal, RenameModal, SettingsModal, @@ -28,6 +30,7 @@ import { clampRatio, collectDividers, computeRects, + dividerDragKey, insertAsNewLeaf, insertBeside, leaves, @@ -102,7 +105,7 @@ type DropPreview = { paneId: string; zone: ReturnType }; type Modal = | { kind: "team"; firstRun: boolean } | { kind: "agent" } - | { kind: "appuser" } + | { kind: "appuser"; auto: boolean } | { kind: "rename"; current: string } | { kind: "leave"; name: string } | { kind: "settings" } @@ -119,6 +122,22 @@ const ROOM_PAGE_SIZE = 30; // enough for now; panes always start fresh each launch anyway (they're // live PTYs, not something a restart could restore even if we tried). const LAST_TEAM_KEY = "agmsg-app-last-team"; +// Persists the two View menu toggles below. See the seeding useEffect for +// why restoring these pushes INTO Rust (set_team_room_visible/ +// set_user_chat_visible) rather than reading view_visibility unconditionally. +const SHOW_TEAM_ROOM_KEY = "agmsg-app-show-team-room"; +const SHOW_USER_CHAT_KEY = "agmsg-app-show-user-chat"; +// Persists the sidebar's icon-only-rail collapse toggle. +const SIDEBAR_COLLAPSED_KEY = "agmsg-app-sidebar-collapsed"; +// Persists the terminal font size (Settings modal). xterm.js default is 15; +// this app has always hardcoded 12 (a bit denser), so that's the fallback. +const TERMINAL_FONT_SIZE_KEY = "agmsg-app-terminal-font-size"; +const DEFAULT_TERMINAL_FONT_SIZE = 12; +// "Don't show this again" for the auto-triggered app-user prompt (see the +// team-change effect below) — adding an app-user is always reachable from +// the chat's "Add one" link, so a user who dismisses the auto-prompt once +// shouldn't be re-asked on every future team switch. +const SUPPRESS_APPUSER_PROMPT_KEY = "agmsg-app-suppress-appuser-prompt"; // Custom drag-and-drop MIME type for pane-swap drags (see PANE_DRAG_MIME // usages below) — a made-up type, not text/plain, so a stray OS file drag // or an unrelated drag elsewhere on the page never accidentally matches a @@ -166,10 +185,24 @@ export default function App() { const [spawnTypes, setSpawnTypes] = useState([]); const [sidebarWidth, setSidebarWidth] = useState(200); const [chatHeight, setChatHeight] = useState(160); + // Terminal font size, adjustable from the Settings modal and persisted + // across restarts (see TERMINAL_FONT_SIZE_KEY). Validated against the same + // range the Settings enforces — a stray/corrupted localStorage + // value (NaN, Infinity, out of range) would otherwise reach xterm as-is + // and persist itself right back on the next write. + const [terminalFontSize, setTerminalFontSize] = useState(() => { + const stored = Number(localStorage.getItem(TERMINAL_FONT_SIZE_KEY)); + return Number.isFinite(stored) && stored >= MIN_TERMINAL_FONT_SIZE && stored <= MAX_TERMINAL_FONT_SIZE + ? stored + : DEFAULT_TERMINAL_FONT_SIZE; + }); // Collapses the team sidebar to an icon-only rail so panes get more width. - // Session-only (no persistence needed) — spawning/messaging a member isn't - // offered from the collapsed rail; expand to get back to the full list. - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + // Persisted across restarts (see SIDEBAR_COLLAPSED_KEY) — spawning/ + // messaging a member isn't offered from the collapsed rail; expand to get + // back to the full list. + const [sidebarCollapsed, setSidebarCollapsed] = useState( + () => localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "true", + ); // Popup the collapsed rail's team-icon button opens — team switching, the // one thing the full sidebar's team enforces (see terminalFontSize's lazy +// useState initializer). +export const MIN_TERMINAL_FONT_SIZE = 8; +export const MAX_TERMINAL_FONT_SIZE = 24; + +export function SettingsModal(props: { + onClose: () => void; + terminalFontSize: number; + onTerminalFontSizeChange: (size: number) => void; +}) { const { t, i18n } = useTranslation(); return ( @@ -384,6 +391,21 @@ export function SettingsModal(props: { onClose: () => void }) { ))} +