From 77c506970f376f5a078a3946994105fd85a2b2b0 Mon Sep 17 00:00:00 2001 From: Kiran Johns Date: Tue, 5 Aug 2025 01:19:48 +0530 Subject: [PATCH 01/33] light and dark themed titlebar --- src-tauri/src/main.rs | 20 +++++++++++++++++++- src-tauri/tauri.conf.json | 4 +++- src/contexts/ThemeContext.tsx | 30 +++++++++++++++++++++++++----- src/lib/api.ts | 14 ++++++++++++++ 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0589bef7..ac7b3889 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -45,7 +45,22 @@ use commands::storage::{ use commands::proxy::{get_proxy_settings, save_proxy_settings, apply_proxy_settings}; use process::ProcessRegistryState; use std::sync::Mutex; -use tauri::Manager; +use tauri::{Manager, Theme}; + +#[tauri::command] +async fn set_window_theme(window: tauri::Window, theme: String) -> Result<(), String> { + let theme_enum = match theme.as_str() { + "dark" => Some(Theme::Dark), + "light" => Some(Theme::Light), + _ => None, + }; + + if let Some(theme) = theme_enum { + window.set_theme(Some(theme)).map_err(|e| e.to_string())?; + } + + Ok(()) +} fn main() { // Initialize logger @@ -249,6 +264,9 @@ fn main() { // Proxy Settings get_proxy_settings, save_proxy_settings, + + // Window Theme + set_window_theme, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0530fc2a..f2374019 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,7 +14,9 @@ { "title": "Claudia", "width": 800, - "height": 600 + "height": 600, + "titleBarStyle": "transparent", + "theme": "dark" } ], "security": { diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index c773e4ed..c33aa211 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -72,7 +72,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre if (savedTheme) { const themeMode = savedTheme as ThemeMode; setThemeState(themeMode); - applyTheme(themeMode, customColors); + await applyTheme(themeMode, customColors); } // Load custom colors @@ -82,7 +82,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre const colors = JSON.parse(savedColors) as CustomThemeColors; setCustomColorsState(colors); if (theme === 'custom') { - applyTheme('custom', colors); + await applyTheme('custom', colors); } } } catch (error) { @@ -96,7 +96,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre }, []); // Apply theme to document - const applyTheme = useCallback((themeMode: ThemeMode, colors: CustomThemeColors) => { + const applyTheme = useCallback(async (themeMode: ThemeMode, colors: CustomThemeColors) => { const root = document.documentElement; // Remove all theme classes @@ -118,6 +118,26 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre root.style.removeProperty(cssVarName); }); } + + // Update window theme to match app theme + try { + // Map themes to window theme (only dark/light supported) + let windowTheme: string; + switch (themeMode) { + case 'light': + windowTheme = 'light'; + break; + case 'dark': + case 'gray': + case 'custom': + default: + windowTheme = 'dark'; + break; + } + await api.setWindowTheme(windowTheme); + } catch (error) { + console.error('Failed to update window theme:', error); + } }, []); const setTheme = useCallback(async (newTheme: ThemeMode) => { @@ -126,7 +146,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre // Apply theme immediately setThemeState(newTheme); - applyTheme(newTheme, customColors); + await applyTheme(newTheme, customColors); // Save to storage await api.saveSetting(THEME_STORAGE_KEY, newTheme); @@ -146,7 +166,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre // Apply immediately if custom theme is active if (theme === 'custom') { - applyTheme('custom', newColors); + await applyTheme('custom', newColors); } // Save to storage diff --git a/src/lib/api.ts b/src/lib/api.ts index 9a78640c..44cd581d 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1879,5 +1879,19 @@ export const api = { console.error("Failed to delete slash command:", error); throw error; } + }, + + /** + * Sets the window theme to match the app theme + * @param theme - The theme to apply: 'dark', 'light', or 'gray' + * @returns Promise resolving when the theme is applied + */ + async setWindowTheme(theme: string): Promise { + try { + return await invoke("set_window_theme", { theme }); + } catch (error) { + console.error("Failed to set window theme:", error); + throw error; + } } }; From 5f810fbd4c84cacecb0837d85a6184f7af5f8e38 Mon Sep 17 00:00:00 2001 From: Kiran Johns Date: Tue, 5 Aug 2025 01:51:54 +0530 Subject: [PATCH 02/33] custom navbar --- src-tauri/Cargo.lock | 85 +++++++++--- src-tauri/Cargo.toml | 3 +- src-tauri/capabilities/default.json | 7 +- src-tauri/src/main.rs | 30 ++--- src-tauri/tauri.conf.json | 6 +- src/App.tsx | 17 ++- src/components/App.cleaned.tsx | 16 ++- src/components/CustomTitlebar.tsx | 192 ++++++++++++++++++++++++++++ src/components/TabManager.tsx | 4 +- src/components/Topbar.tsx | 66 +--------- src/contexts/ThemeContext.tsx | 20 +-- src/lib/api.ts | 13 -- 12 files changed, 320 insertions(+), 139 deletions(-) create mode 100644 src/components/CustomTitlebar.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5c3951ef..4ba76ca4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -143,7 +143,7 @@ dependencies = [ "image", "log", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", @@ -653,6 +653,7 @@ dependencies = [ "uuid", "walkdir", "which", + "window-vibrancy 0.5.3", "zstd", ] @@ -1768,7 +1769,7 @@ dependencies = [ "crossbeam-channel", "keyboard-types", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "once_cell", "serde", "thiserror 2.0.12", @@ -2663,7 +2664,7 @@ dependencies = [ "gtk", "keyboard-types", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "once_cell", @@ -2840,6 +2841,22 @@ dependencies = [ "objc2-exception-helper", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + [[package]] name = "objc2-app-kit" version = "0.3.1" @@ -2851,10 +2868,10 @@ dependencies = [ "libc", "objc2 0.6.1", "objc2-cloud-kit", - "objc2-core-data", + "objc2-core-data 0.3.1", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image", + "objc2-core-image 0.3.1", "objc2-foundation 0.3.1", "objc2-quartz-core 0.3.1", ] @@ -2870,6 +2887,18 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-data" version = "0.3.1" @@ -2905,6 +2934,18 @@ dependencies = [ "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + [[package]] name = "objc2-core-image" version = "0.3.1" @@ -2986,7 +3027,7 @@ checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ "bitflags 2.9.1", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", ] @@ -3035,7 +3076,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.6.1", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", ] @@ -3903,7 +3944,7 @@ dependencies = [ "js-sys", "log", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "raw-window-handle", @@ -4586,7 +4627,7 @@ dependencies = [ "ndk-context", "ndk-sys", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "once_cell", "parking_lot", @@ -4654,7 +4695,7 @@ dependencies = [ "mime", "muda", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "objc2-ui-kit", "percent-encoding", @@ -4678,7 +4719,7 @@ dependencies = [ "urlpattern", "webkit2gtk", "webview2-com", - "window-vibrancy", + "window-vibrancy 0.6.0", "windows", ] @@ -4971,7 +5012,7 @@ dependencies = [ "jni", "log", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-foundation 0.3.1", "once_cell", "percent-encoding", @@ -5398,7 +5439,7 @@ dependencies = [ "libappindicator", "muda", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", @@ -5960,6 +6001,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window-vibrancy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831ad7678290beae36be6f9fad9234139c7f00f3b536347de7745621716be82d" +dependencies = [ + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + [[package]] name = "window-vibrancy" version = "0.6.0" @@ -5967,7 +6022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "raw-window-handle", @@ -6483,7 +6538,7 @@ dependencies = [ "libc", "ndk", "objc2 0.6.1", - "objc2-app-kit", + "objc2-app-kit 0.3.1", "objc2-core-foundation", "objc2-foundation 0.3.1", "objc2-ui-kit", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 01efb811..62ef82f0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,7 +20,7 @@ crate-type = ["lib", "cdylib", "staticlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] } +tauri = { version = "2", features = [ "macos-private-api", "protocol-asset", "tray-icon", "image-png"] } tauri-plugin-shell = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" @@ -30,6 +30,7 @@ tauri-plugin-notification = "2" tauri-plugin-clipboard-manager = "2" tauri-plugin-global-shortcut = "2" tauri-plugin-http = "2" +window-vibrancy = "0.5" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 0eeb8f72..fda6ea66 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -49,6 +49,11 @@ "notification:default", "clipboard-manager:default", "global-shortcut:default", - "updater:default" + "updater:default", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-close", + "core:window:allow-is-maximized" ] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ac7b3889..b5ccf96b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -45,22 +45,11 @@ use commands::storage::{ use commands::proxy::{get_proxy_settings, save_proxy_settings, apply_proxy_settings}; use process::ProcessRegistryState; use std::sync::Mutex; -use tauri::{Manager, Theme}; +use tauri::Manager; + +#[cfg(target_os = "macos")] +use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; -#[tauri::command] -async fn set_window_theme(window: tauri::Window, theme: String) -> Result<(), String> { - let theme_enum = match theme.as_str() { - "dark" => Some(Theme::Dark), - "light" => Some(Theme::Light), - _ => None, - }; - - if let Some(theme) = theme_enum { - window.set_theme(Some(theme)).map_err(|e| e.to_string())?; - } - - Ok(()) -} fn main() { // Initialize logger @@ -151,6 +140,14 @@ fn main() { // Initialize Claude process state app.manage(ClaudeProcessState::default()); + // Apply window vibrancy with rounded corners on macOS + #[cfg(target_os = "macos")] + { + let window = app.get_webview_window("main").unwrap(); + apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(12.0)) + .expect("Failed to apply window vibrancy"); + } + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -264,9 +261,6 @@ fn main() { // Proxy Settings get_proxy_settings, save_proxy_settings, - - // Window Theme - set_window_theme, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f2374019..e2788373 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -10,13 +10,15 @@ "frontendDist": "../dist" }, "app": { + "macOSPrivateApi": true, "windows": [ { "title": "Claudia", "width": 800, "height": 600, - "titleBarStyle": "transparent", - "theme": "dark" + "decorations": false, + "transparent": true, + "shadow": true } ], "security": { diff --git a/src/App.tsx b/src/App.tsx index 0d424fb7..d3eef6d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { ProjectList } from "@/components/ProjectList"; import { SessionList } from "@/components/SessionList"; import { RunningClaudeSessions } from "@/components/RunningClaudeSessions"; import { Topbar } from "@/components/Topbar"; +import { CustomTitlebar } from "@/components/CustomTitlebar"; import { MarkdownEditor } from "@/components/MarkdownEditor"; import { ClaudeFileEditor } from "@/components/ClaudeFileEditor"; import { Settings } from "@/components/Settings"; @@ -476,15 +477,25 @@ function AppContent() { return (
- {/* Topbar */} - setShowAgentsModal(true)} + onUsageClick={() => createUsageTab()} + onClaudeClick={() => createClaudeMdTab()} + onMCPClick={() => createMCPTab()} + onSettingsClick={() => createSettingsTab()} + onInfoClick={() => setShowNFO(true)} + /> + + {/* Topbar - Commented out since navigation moved to titlebar */} + {/* createClaudeMdTab()} onSettingsClick={() => createSettingsTab()} onUsageClick={() => createUsageTab()} onMCPClick={() => createMCPTab()} onInfoClick={() => setShowNFO(true)} onAgentsClick={() => setShowAgentsModal(true)} - /> + /> */} {/* Analytics Consent Banner */} diff --git a/src/components/App.cleaned.tsx b/src/components/App.cleaned.tsx index b37c0179..5abd091c 100644 --- a/src/components/App.cleaned.tsx +++ b/src/components/App.cleaned.tsx @@ -8,6 +8,7 @@ import { Toast, ToastContainer } from "@/components/ui/toast"; import { TabManager } from "@/components/TabManager"; import { TabContent } from "@/components/TabContent"; import { AgentsModal } from "@/components/AgentsModal"; +import { CustomTitlebar } from "@/components/CustomTitlebar"; import { useTabState } from "@/hooks/useTabState"; /** @@ -111,8 +112,21 @@ function AppContent() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="min-h-screen bg-background flex flex-col" + className="min-h-screen bg-background flex flex-col rounded-xl overflow-hidden shadow-2xl border border-border/20" > + {/* Custom Titlebar */} + { + // Open settings tab or modal + window.dispatchEvent(new CustomEvent('create-settings-tab')); + }} + onAgentsClick={() => setShowAgentsModal(true)} + onMenuClick={() => { + // Could open a command palette or menu + console.log('Menu clicked'); + }} + /> + {/* Tab-based interface */}
diff --git a/src/components/CustomTitlebar.tsx b/src/components/CustomTitlebar.tsx new file mode 100644 index 00000000..d1b2a4c5 --- /dev/null +++ b/src/components/CustomTitlebar.tsx @@ -0,0 +1,192 @@ +import React, { useState } from 'react'; +import { Settings, Users, Menu, Minus, Square, X, Bot, BarChart3, FileText, Network, Info } from 'lucide-react'; +import { useThemeContext } from '@/contexts/ThemeContext'; +import { getCurrentWindow } from '@tauri-apps/api/window'; + +interface CustomTitlebarProps { + title?: string; + onSettingsClick?: () => void; + onAgentsClick?: () => void; + onUsageClick?: () => void; + onClaudeClick?: () => void; + onMCPClick?: () => void; + onInfoClick?: () => void; + onMenuClick?: () => void; +} + +export const CustomTitlebar: React.FC = ({ + title = "Claudia", + onSettingsClick, + onAgentsClick, + onUsageClick, + onClaudeClick, + onMCPClick, + onInfoClick, + onMenuClick +}) => { + const { theme } = useThemeContext(); + const [isHovered, setIsHovered] = useState(false); + + const handleMinimize = async () => { + try { + const window = getCurrentWindow(); + await window.minimize(); + console.log('Window minimized successfully'); + } catch (error) { + console.error('Failed to minimize window:', error); + } + }; + + const handleMaximize = async () => { + try { + const window = getCurrentWindow(); + const isMaximized = await window.isMaximized(); + if (isMaximized) { + await window.unmaximize(); + console.log('Window unmaximized successfully'); + } else { + await window.maximize(); + console.log('Window maximized successfully'); + } + } catch (error) { + console.error('Failed to maximize/unmaximize window:', error); + } + }; + + const handleClose = async () => { + try { + const window = getCurrentWindow(); + await window.close(); + console.log('Window closed successfully'); + } catch (error) { + console.error('Failed to close window:', error); + } + }; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Left side - macOS Traffic Light buttons */} +
+
+ {/* Close button */} + + + {/* Minimize button */} + + + {/* Maximize button */} + +
+
+ + {/* Center - Title */} +
+ {title} +
+ + {/* Right side - Navigation icons */} +
+ {onAgentsClick && ( + + )} + + {onUsageClick && ( + + )} + + {onClaudeClick && ( + + )} + + {onMCPClick && ( + + )} + + {onSettingsClick && ( + + )} + + {onInfoClick && ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/TabManager.tsx b/src/components/TabManager.tsx index b82e8b33..21e49edf 100644 --- a/src/components/TabManager.tsx +++ b/src/components/TabManager.tsx @@ -390,9 +390,9 @@ export const TabManager: React.FC = ({ className }) => { disabled={!canAddTab()} className={cn( "p-2 mx-2 rounded-md transition-all duration-200 flex items-center justify-center", - "border border-border/50 bg-background/50 backdrop-blur-sm", + "bg-background/50 backdrop-blur-sm", canAddTab() - ? "hover:bg-muted/80 hover:border-border text-muted-foreground hover:text-foreground hover:shadow-sm" + ? "hover:bg-muted/80 text-muted-foreground hover:text-foreground hover:shadow-sm" : "opacity-50 cursor-not-allowed bg-muted/30" )} title={canAddTab() ? "Browse projects (Ctrl+T)" : `Maximum tabs reached (${tabs.length}/20)`} diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index 8917d7db..dbcf9128 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -176,70 +176,8 @@ export const Topbar: React.FC = ({ {/* Status Indicator */} - {/* Action Buttons */} -
- {onAgentsClick && ( - - )} - - - - - - - - - - -
+ {/* Spacer - Navigation moved to titlebar */} +
); }; \ No newline at end of file diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index c33aa211..7520cfa6 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -119,25 +119,7 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre }); } - // Update window theme to match app theme - try { - // Map themes to window theme (only dark/light supported) - let windowTheme: string; - switch (themeMode) { - case 'light': - windowTheme = 'light'; - break; - case 'dark': - case 'gray': - case 'custom': - default: - windowTheme = 'dark'; - break; - } - await api.setWindowTheme(windowTheme); - } catch (error) { - console.error('Failed to update window theme:', error); - } + // Note: Window theme updates removed since we're using custom titlebar }, []); const setTheme = useCallback(async (newTheme: ThemeMode) => { diff --git a/src/lib/api.ts b/src/lib/api.ts index 44cd581d..894b90bc 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1881,17 +1881,4 @@ export const api = { } }, - /** - * Sets the window theme to match the app theme - * @param theme - The theme to apply: 'dark', 'light', or 'gray' - * @returns Promise resolving when the theme is applied - */ - async setWindowTheme(theme: string): Promise { - try { - return await invoke("set_window_theme", { theme }); - } catch (error) { - console.error("Failed to set window theme:", error); - throw error; - } - } }; From b74cc7fffede5a0398c48a89f5c3bdb0f3601edf Mon Sep 17 00:00:00 2001 From: Kiran Johns Date: Wed, 6 Aug 2025 22:45:57 +0530 Subject: [PATCH 03/33] move icons to the titlebar --- src-tauri/Cargo.lock | 69 +++++++++++++++++++++++++++---- src-tauri/Cargo.toml | 2 + src-tauri/src/main.rs | 25 ++++++++++- src/App.tsx | 2 +- src/components/CustomTitlebar.tsx | 9 ++-- src/components/TabContent.tsx | 2 +- src/components/TabManager.tsx | 2 +- 7 files changed, 95 insertions(+), 16 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4ba76ca4..f907bb50 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -622,7 +622,8 @@ dependencies = [ "async-trait", "base64 0.22.1", "chrono", - "cocoa", + "cocoa 0.25.0", + "cocoa 0.26.1", "dirs 5.0.1", "env_logger", "futures", @@ -666,6 +667,22 @@ dependencies = [ "error-code", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.26.1" @@ -674,14 +691,28 @@ checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ "bitflags 2.9.1", "block", - "cocoa-foundation", + "cocoa-foundation 0.2.1", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "libc", "objc", ] +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "cocoa-foundation" version = "0.2.1" @@ -691,7 +722,7 @@ dependencies = [ "bitflags 2.9.1", "block", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "objc", ] @@ -781,6 +812,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" @@ -789,11 +833,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -4427,7 +4482,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types 0.5.0", "js-sys", "log", @@ -4611,7 +4666,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 62ef82f0..4eeaf718 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,6 +31,8 @@ tauri-plugin-clipboard-manager = "2" tauri-plugin-global-shortcut = "2" tauri-plugin-http = "2" window-vibrancy = "0.5" +cocoa = "0.25" +objc = "0.2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b5ccf96b..10e9c74a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -144,8 +144,29 @@ fn main() { #[cfg(target_os = "macos")] { let window = app.get_webview_window("main").unwrap(); - apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(12.0)) - .expect("Failed to apply window vibrancy"); + + // Try different vibrancy materials that support rounded corners + let materials = [ + NSVisualEffectMaterial::UnderWindowBackground, + NSVisualEffectMaterial::WindowBackground, + NSVisualEffectMaterial::Popover, + NSVisualEffectMaterial::Menu, + NSVisualEffectMaterial::Sidebar, + ]; + + let mut applied = false; + for material in materials.iter() { + if apply_vibrancy(&window, *material, None, Some(12.0)).is_ok() { + applied = true; + break; + } + } + + if !applied { + // Fallback without rounded corners + apply_vibrancy(&window, NSVisualEffectMaterial::WindowBackground, None, None) + .expect("Failed to apply any window vibrancy"); + } } Ok(()) diff --git a/src/App.tsx b/src/App.tsx index d3eef6d6..7ad41a30 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -476,7 +476,7 @@ function AppContent() { }; return ( -
+
{/* Custom Titlebar */} setShowAgentsModal(true)} diff --git a/src/components/CustomTitlebar.tsx b/src/components/CustomTitlebar.tsx index d1b2a4c5..547855f6 100644 --- a/src/components/CustomTitlebar.tsx +++ b/src/components/CustomTitlebar.tsx @@ -65,7 +65,8 @@ export const CustomTitlebar: React.FC = ({ return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -117,13 +118,13 @@ export const CustomTitlebar: React.FC = ({
- {/* Center - Title */} -
{title} -
+
*/} {/* Right side - Navigation icons */}
diff --git a/src/components/TabContent.tsx b/src/components/TabContent.tsx index cb5ca82d..4c2a8eaa 100644 --- a/src/components/TabContent.tsx +++ b/src/components/TabContent.tsx @@ -401,7 +401,7 @@ export const TabContent: React.FC = () => { }, [createChatTab, findTabBySessionId, createClaudeFileTab, createAgentExecutionTab, createCreateAgentTab, createImportAgentTab, closeTab, updateTab]); return ( -
+
{tabs.map((tab) => ( = ({ className }) => { }; return ( -
+
{/* Left fade gradient */} {showLeftScroll && (
From 4c638efbb3a2662b5b2abacad4038deeb2a3df13 Mon Sep 17 00:00:00 2001 From: Kiran Johns Date: Wed, 6 Aug 2025 23:41:02 +0530 Subject: [PATCH 04/33] ui updates to project and session listings --- README.md | 4 +- src/App.tsx | 6 +- src/components/ProjectList.tsx | 233 +++++++++++++++++++++------------ src/components/SessionList.tsx | 174 +++++++++++++----------- src/components/TabContent.tsx | 38 +++--- src/contexts/TabContext.tsx | 4 +- src/hooks/useTabState.ts | 2 +- 7 files changed, 274 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index 0b8f4893..a7f4d669 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,13 @@ Think of Claudia as your command center for Claude Code - bridging the gap betwe ### Getting Started 1. **Launch Claudia**: Open the application after installation -2. **Welcome Screen**: Choose between CC Agents or CC Projects +2. **Welcome Screen**: Choose between CC Agents or Projects 3. **First Time Setup**: Claudia will automatically detect your `~/.claude` directory ### Managing Projects ``` -CC Projects → Select Project → View Sessions → Resume or Start New +Projects → Select Project → View Sessions → Resume or Start New ``` - Click on any project to view its sessions diff --git a/src/App.tsx b/src/App.tsx index 7ad41a30..80521f13 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -270,7 +270,7 @@ function AppContent() { - {/* CC Projects Card */} + {/* Projects Card */}
-

CC Projects

+

Projects

@@ -333,7 +333,7 @@ function AppContent() { ← Back to Home
-

CC Projects

+

Projects

Browse your Claude Code sessions

diff --git a/src/components/ProjectList.tsx b/src/components/ProjectList.tsx index 058de960..54285d54 100644 --- a/src/components/ProjectList.tsx +++ b/src/components/ProjectList.tsx @@ -6,7 +6,9 @@ import { FileText, ChevronRight, Settings, - MoreVertical + MoreVertical, + Clock, + Activity } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; @@ -17,6 +19,12 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import type { Project } from "@/lib/api"; import { cn } from "@/lib/utils"; import { formatTimeAgo } from "@/lib/date-utils"; @@ -55,6 +63,33 @@ const getProjectName = (path: string): string => { return parts[parts.length - 1] || path; }; +/** + * Formats path to be more readable + */ +const getDisplayPath = (path: string): string => { + // Try to make path home-relative + const homeIndicators = ['/Users/', '/home/']; + for (const indicator of homeIndicators) { + if (path.includes(indicator)) { + const parts = path.split('/'); + const userIndex = parts.findIndex((p, i) => + i > 0 && parts[i - 1] === indicator.split('/')[1] + ); + if (userIndex > 0) { + return '~/' + parts.slice(userIndex + 1).join('/'); + } + } + } + + // Fallback to showing last 2-3 segments for very long paths + const parts = path.split('/').filter(Boolean); + if (parts.length > 3) { + return '.../' + parts.slice(-2).join('/'); + } + + return path; +}; + /** * ProjectList component - Displays a paginated list of projects with hover animations * @@ -83,92 +118,124 @@ export const ProjectList: React.FC = ({ setCurrentPage(1); }, [projects.length]); + // Get the most recent session for each project + const getRecentActivity = (project: Project) => { + if (project.sessions.length === 0) return null; + // Assuming sessions are sorted by date, get the most recent one + return project.sessions[0]; + }; + return ( -
-
- {currentProjects.map((project, index) => ( - - onProjectClick(project)} - > -
-
-
-
- -

- {getProjectName(project.path)} -

-
- {project.sessions.length > 0 && ( - - {project.sessions.length} - - )} -
- -

- {project.path} -

-
- -
-
-
- - {formatTimeAgo(project.created_at * 1000)} + +
+
+ {currentProjects.map((project, index) => { + const recentSession = getRecentActivity(project); + const hasActivity = project.sessions.length > 0; + + return ( + + onProjectClick(project)} + > +
+
+ {/* Project header */} +
+
+ +

+ {getProjectName(project.path)} +

+
+ {project.sessions.length > 0 && ( + 5 ? "default" : "secondary"} + className="text-xs px-1.5 py-0 h-5" + > + {project.sessions.length} + + )} +
+ + {/* Path display */} +

+ {getDisplayPath(project.path)} +

+ + {/* Activity indicator */} + {hasActivity && recentSession && ( +
+ + + {recentSession.first_message + ? recentSession.first_message.slice(0, 40) + '...' + : 'Active session'} + +
+ )}
-
- - {project.sessions.length} + + {/* Footer */} +
+
+ + {formatTimeAgo(project.created_at * 1000)} +
+ +
+ {onProjectSettings && ( + + e.stopPropagation()}> + + + + { + e.stopPropagation(); + onProjectSettings(project); + }} + > + + Hooks + + + + )} + +
- -
- {onProjectSettings && ( - - e.stopPropagation()}> - - - - { - e.stopPropagation(); - onProjectSettings(project); - }} - > - - Hooks - - - - )} - -
-
-
- - - ))} + + + ); + })} +
+ +
- - -
+ ); }; diff --git a/src/components/SessionList.tsx b/src/components/SessionList.tsx index 7b0a2827..17ca0427 100644 --- a/src/components/SessionList.tsx +++ b/src/components/SessionList.tsx @@ -1,10 +1,16 @@ import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { FileText, ArrowLeft, Calendar, Clock, MessageSquare } from "lucide-react"; +import { FileText, ArrowLeft, Calendar, Clock, MessageSquare, Info } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Pagination } from "@/components/ui/pagination"; import { ClaudeMemoriesDropdown } from "@/components/ClaudeMemoriesDropdown"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { formatUnixTimestamp, formatISOTimestamp, truncateText, getFirstLine } from "@/lib/date-utils"; import type { Session, ClaudeMdFile } from "@/lib/api"; @@ -36,7 +42,7 @@ interface SessionListProps { className?: string; } -const ITEMS_PER_PAGE = 5; +const ITEMS_PER_PAGE = 12; /** * SessionList component - Displays paginated sessions for a specific project @@ -71,28 +77,41 @@ export const SessionList: React.FC = ({ }, [sessions.length]); return ( -
- - -
-

{projectPath}

-

- {sessions.length} session{sessions.length !== 1 ? 's' : ''} -

-
-
+ +
+
+

+ {projectPath.split('/').pop() || 'Project'} +

+

+ {sessions.length} session{sessions.length !== 1 ? 's' : ''} +

+
+ + + + + +

{projectPath}

+
+
+
+ {/* CLAUDE.md Memories Dropdown */} {onEditClaudeFile && ( @@ -109,7 +128,7 @@ export const SessionList: React.FC = ({ )} -
+
{currentSessions.map((session, index) => ( = ({ > { // Emit a special event for Claude Code session navigation @@ -136,63 +155,70 @@ export const SessionList: React.FC = ({ onSessionClick?.(session); }} > - -
-
-
- -
-

{session.id}

- - {/* First message preview */} - {session.first_message && ( -
-
- - First message: -
-

- {truncateText(getFirstLine(session.first_message), 100)} -

-
- )} - - {/* Metadata */} -
- {/* Message timestamp if available, otherwise file creation time */} -
- - - {session.message_timestamp - ? formatISOTimestamp(session.message_timestamp) - : formatUnixTimestamp(session.created_at) - } - -
- - {session.todo_data && ( -
- - Has todo -
- )} -
+
+
+ {/* Session header */} +
+
+ +
+

+ Session on {session.message_timestamp + ? new Date(session.message_timestamp).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }) + : new Date(session.created_at * 1000).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }) + } +

+ {session.todo_data && ( + + Todo + + )}
+ + {/* First message preview */} + {session.first_message ? ( +

+ {truncateText(getFirstLine(session.first_message), 120)} +

+ ) : ( +

+ No messages yet +

+ )} +
+ + {/* Metadata footer */} +
+

+ {session.id.slice(-8)} +

+ {session.todo_data && ( + + )}
- +
))}
- -
+ +
+ ); }; \ No newline at end of file diff --git a/src/components/TabContent.tsx b/src/components/TabContent.tsx index 4c2a8eaa..45f03575 100644 --- a/src/components/TabContent.tsx +++ b/src/components/TabContent.tsx @@ -96,10 +96,21 @@ const TabPanel: React.FC = ({ tab, isActive }) => {
{/* Header */}
-

CC Projects

-

- Browse your Claude Code sessions -

+
+
+

Projects

+

+ Browse your Claude Code sessions +

+
+ +
{/* Error display */} @@ -161,23 +172,6 @@ const TabPanel: React.FC = ({ tab, isActive }) => { exit={{ opacity: 0, x: 20 }} transition={{ duration: 0.3 }} > - {/* New session button at the top */} - - - - {/* Running Claude Sessions */} @@ -217,7 +211,7 @@ const TabPanel: React.FC = ({ tab, isActive }) => { // Go back to projects view in the same tab updateTab(tab.id, { type: 'projects', - title: 'CC Projects', + title: 'Projects', }); }} /> diff --git a/src/contexts/TabContext.tsx b/src/contexts/TabContext.tsx index 73fc33bc..d0633a44 100644 --- a/src/contexts/TabContext.tsx +++ b/src/contexts/TabContext.tsx @@ -40,13 +40,13 @@ export const TabProvider: React.FC<{ children: React.ReactNode }> = ({ children const [tabs, setTabs] = useState([]); const [activeTabId, setActiveTabId] = useState(null); - // Always start with a fresh CC Projects tab + // Always start with a fresh Projects tab useEffect(() => { // Create default projects tab const defaultTab: Tab = { id: generateTabId(), type: 'projects', - title: 'CC Projects', + title: 'Projects', status: 'idle', hasUnsavedChanges: false, order: 0, diff --git a/src/hooks/useTabState.ts b/src/hooks/useTabState.ts index a3e650f8..61332e90 100644 --- a/src/hooks/useTabState.ts +++ b/src/hooks/useTabState.ts @@ -100,7 +100,7 @@ export const useTabState = (): UseTabStateReturn => { return addTab({ type: 'projects', - title: 'CC Projects', + title: 'Projects', status: 'idle', hasUnsavedChanges: false, icon: 'folder' From c138d8d01024461b496c4fb249909750ff93f6f7 Mon Sep 17 00:00:00 2001 From: Kiran Johns Date: Wed, 6 Aug 2025 23:55:50 +0530 Subject: [PATCH 05/33] made changes to session creation flow and fixed duplicate tab opening --- src/App.tsx | 9 ----- src/components/ClaudeCodeSession.tsx | 7 ++++ src/components/SessionList.tsx | 41 ++-------------------- src/components/TabContent.tsx | 51 +++++++++++++++++----------- src/hooks/useTabState.ts | 5 +-- 5 files changed, 44 insertions(+), 69 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 80521f13..ca3f910d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -191,14 +191,6 @@ function AppContent() { // The tab system will handle creating a new chat tab }; - /** - * Returns to project list view - */ - const handleBack = () => { - setSelectedProject(null); - setSessions([]); - }; - /** * Handles editing a CLAUDE.md file from a project */ @@ -372,7 +364,6 @@ function AppContent() { diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index 20d32a00..e73a5c55 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -306,6 +306,13 @@ export const ClaudeCodeSession: React.FC = ({ // After loading history, we're continuing a conversation setIsFirstPrompt(false); + + // Scroll to bottom after loading history + setTimeout(() => { + if (loadedMessages.length > 0) { + rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); + } + }, 100); } catch (err) { console.error("Failed to load session history:", err); setError("Failed to load session history"); diff --git a/src/components/SessionList.tsx b/src/components/SessionList.tsx index 17ca0427..176c287f 100644 --- a/src/components/SessionList.tsx +++ b/src/components/SessionList.tsx @@ -1,8 +1,7 @@ import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { FileText, ArrowLeft, Calendar, Clock, MessageSquare, Info } from "lucide-react"; +import { FileText, Calendar, Clock, MessageSquare, Info } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; import { Pagination } from "@/components/ui/pagination"; import { ClaudeMemoriesDropdown } from "@/components/ClaudeMemoriesDropdown"; import { @@ -25,9 +24,9 @@ interface SessionListProps { */ projectPath: string; /** - * Callback to go back to project list + * Optional callback to go back to project list (deprecated - use tabs instead) */ - onBack: () => void; + onBack?: () => void; /** * Callback when a session is clicked */ @@ -79,40 +78,6 @@ export const SessionList: React.FC = ({ return (
- - -
-
-

- {projectPath.split('/').pop() || 'Project'} -

-

- {sessions.length} session{sessions.length !== 1 ? 's' : ''} -

-
- - - - - -

{projectPath}

-
-
-
-
- {/* CLAUDE.md Memories Dropdown */} {onEditClaudeFile && ( = ({ tab, isActive }) => { } }; - const handleBack = () => { - setSelectedProject(null); - setSessions([]); - }; - const handleNewSession = () => { - // Create a new chat tab - createChatTab(); + // Create a new chat tab with the current project path if available + if (selectedProject) { + const projectName = selectedProject.path.split('/').pop() || 'Session'; + createChatTab(undefined, projectName, selectedProject.path); + } else { + createChatTab(); + } }; // Panel visibility - hide when not active @@ -96,11 +96,29 @@ const TabPanel: React.FC = ({ tab, isActive }) => {
{/* Header */}
+ {selectedProject && ( + + )}
-

Projects

+

+ {selectedProject ? selectedProject.path.split('/').pop() : 'Projects'} +

- Browse your Claude Code sessions + {selectedProject + ? `${sessions.length} session${sessions.length !== 1 ? 's' : ''}` + : 'Browse your Claude Code sessions' + }

-
- -
-

Claude Code Session

-

- {projectPath ? `${projectPath}` : "No project selected"} -

-
-
-
- -
- {projectPath && onProjectSettings && ( - - )} - {projectPath && ( - - )} -
- {showSettings && ( - - )} - - - - - - -

Checkpoint Settings

-
-
-
- {effectiveSession && ( - - - - - - -

Timeline Navigator

-
-
-
- )} - {messages.length > 0 && ( - - - Copy Output - - - } - content={ -
- - -
- } - open={copyPopoverOpen} - onOpenChange={setCopyPopoverOpen} - /> - )} -
-
- {/* Main Content Area */}
= ({ /> ) : ( // Original layout when no preview -
+
{projectPathInput} {messagesList} @@ -1571,13 +1438,99 @@ export const ClaudeCodeSession: React.FC = ({ isLoading={isLoading} disabled={!projectPath} projectPath={projectPath} + extraMenuItems={ + <> + {effectiveSession && ( + + + + + + +

Timeline

+
+
+
+ )} + {messages.length > 0 && ( + + + + + + +

Copy

+
+
+ + } + content={ +
+ + +
+ } + open={copyPopoverOpen} + onOpenChange={setCopyPopoverOpen} + side="top" + align="end" + /> + )} + + + + + + +

Checkpoint Settings

+
+
+
+ + } />
{/* Token Counter - positioned under the Send button */} {totalTokens > 0 && (
-
+
void; + /** + * Extra menu items to display in the prompt bar + */ + extraMenuItems?: React.ReactNode; } export interface FloatingPromptInputRef { @@ -70,6 +78,9 @@ type ThinkingModeConfig = { description: string; level: number; // 0-4 for visual indicator phrase?: string; // The phrase to append + icon: React.ReactNode; + color: string; + shortName: string; }; const THINKING_MODES: ThinkingModeConfig[] = [ @@ -77,50 +88,71 @@ const THINKING_MODES: ThinkingModeConfig[] = [ id: "auto", name: "Auto", description: "Let Claude decide", - level: 0 + level: 0, + icon: , + color: "text-muted-foreground", + shortName: "A" }, { id: "think", name: "Think", description: "Basic reasoning", level: 1, - phrase: "think" + phrase: "think", + icon: , + color: "text-primary", + shortName: "T" }, { id: "think_hard", name: "Think Hard", description: "Deeper analysis", level: 2, - phrase: "think hard" + phrase: "think hard", + icon: , + color: "text-primary", + shortName: "T+" }, { id: "think_harder", name: "Think Harder", description: "Extensive reasoning", level: 3, - phrase: "think harder" + phrase: "think harder", + icon: , + color: "text-primary", + shortName: "T++" }, { id: "ultrathink", name: "Ultrathink", description: "Maximum computation", level: 4, - phrase: "ultrathink" + phrase: "ultrathink", + icon: , + color: "text-primary", + shortName: "Ultra" } ]; /** * ThinkingModeIndicator component - Shows visual indicator bars for thinking level */ -const ThinkingModeIndicator: React.FC<{ level: number }> = ({ level }) => { +const ThinkingModeIndicator: React.FC<{ level: number; color?: string }> = ({ level, color }) => { + const getBarColor = (barIndex: number) => { + if (barIndex > level) return "bg-muted"; + return "bg-primary"; + }; + return (
{[1, 2, 3, 4].map((i) => (
))} @@ -133,6 +165,8 @@ type Model = { name: string; description: string; icon: React.ReactNode; + shortName: string; + color: string; }; const MODELS: Model[] = [ @@ -140,13 +174,17 @@ const MODELS: Model[] = [ id: "sonnet", name: "Claude 4 Sonnet", description: "Faster, efficient for most tasks", - icon: + icon: , + shortName: "S", + color: "text-primary" }, { id: "opus", name: "Claude 4 Opus", description: "More capable, better for complex tasks", - icon: + icon: , + shortName: "O", + color: "text-primary" } ]; @@ -170,6 +208,7 @@ const FloatingPromptInputInner = ( projectPath, className, onCancel, + extraMenuItems, }: FloatingPromptInputProps, ref: React.Ref, ) => { @@ -190,6 +229,7 @@ const FloatingPromptInputInner = ( const textareaRef = useRef(null); const expandedTextareaRef = useRef(null); const unlistenDragDropRef = useRef<(() => void) | null>(null); + const [textareaHeight, setTextareaHeight] = useState(48); // Expose a method to add images programmatically React.useImperativeHandle( @@ -294,7 +334,16 @@ const FloatingPromptInputInner = ( const imagePaths = extractImagePaths(prompt); console.log('[useEffect] Setting embeddedImages to:', imagePaths); setEmbeddedImages(imagePaths); - }, [prompt, projectPath]); + + // Auto-resize on prompt change (handles paste, programmatic changes, etc.) + if (textareaRef.current && !isExpanded) { + textareaRef.current.style.height = 'auto'; + const scrollHeight = textareaRef.current.scrollHeight; + const newHeight = Math.min(Math.max(scrollHeight, 48), 240); + setTextareaHeight(newHeight); + textareaRef.current.style.height = `${newHeight}px`; + } + }, [prompt, projectPath, isExpanded]); // Set up Tauri drag-drop event listener useEffect(() => { @@ -396,12 +445,24 @@ const FloatingPromptInputInner = ( onSend(finalPrompt, selectedModel); setPrompt(""); setEmbeddedImages([]); + setTextareaHeight(48); // Reset height after sending } }; const handleTextChange = (e: React.ChangeEvent) => { const newValue = e.target.value; const newCursorPosition = e.target.selectionStart || 0; + + // Auto-resize textarea based on content + if (textareaRef.current && !isExpanded) { + // Reset height to auto to get the actual scrollHeight + textareaRef.current.style.height = 'auto'; + const scrollHeight = textareaRef.current.scrollHeight; + // Set min height to 48px and max to 240px (about 10 lines) + const newHeight = Math.min(Math.max(scrollHeight, 48), 240); + setTextareaHeight(newHeight); + textareaRef.current.style.height = `${newHeight}px`; + } // Check if / was just typed at the beginning of input or after whitespace if (newValue.length > prompt.length && newValue[newCursorPosition - 1] === '/') { @@ -611,6 +672,13 @@ const FloatingPromptInputInner = ( return; } + // Add keyboard shortcut for expanding + if (e.key === 'e' && (e.ctrlKey || e.metaKey) && e.shiftKey) { + e.preventDefault(); + setIsExpanded(true); + return; + } + if (e.key === "Enter" && !e.shiftKey && !isExpanded && !showFilePicker && !showSlashCommandPicker) { e.preventDefault(); handleSend(); @@ -758,7 +826,7 @@ const FloatingPromptInputInner = ( value={prompt} onChange={handleTextChange} onPaste={handlePaste} - placeholder="Type your prompt here..." + placeholder="Type your message..." className="min-h-[200px] resize-none" disabled={disabled} onDragEnter={handleDrag} @@ -777,7 +845,9 @@ const FloatingPromptInputInner = ( onClick={() => setModelPickerOpen(!modelPickerOpen)} className="gap-2" > - {selectedModelData.icon} + + {selectedModelData.icon} + {selectedModelData.name}
@@ -795,7 +865,9 @@ const FloatingPromptInputInner = ( onClick={() => setThinkingModePickerOpen(!thinkingModePickerOpen)} className="gap-2" > - + m.id === selectedThinkingMode)?.color}> + {THINKING_MODES.find(m => m.id === selectedThinkingMode)?.icon} + m.id === selectedThinkingMode)?.level || 0} /> @@ -823,7 +895,9 @@ const FloatingPromptInputInner = ( selectedThinkingMode === mode.id && "bg-accent" )} > - + + {mode.icon} +
{mode.name} @@ -866,7 +940,7 @@ const FloatingPromptInputInner = ( {/* Fixed Position Input Bar */}
-
+
{/* Image previews */} {embeddedImages.length > 0 && ( )} -
-
- {/* Model Picker */} - - {selectedModelData.icon} - {selectedModelData.name} - - - } +
+
+ {/* Model & Thinking Mode Selectors - Left side, fixed at bottom */} +
+ + + + + + +

{selectedModelData.name}

+

{selectedModelData.description}

+
+
+ + } content={
{MODELS.map((model) => ( @@ -916,7 +1005,11 @@ const FloatingPromptInputInner = ( selectedModel === model.id && "bg-accent" )} > -
{model.icon}
+
+ + {model.icon} + +
{model.name}
@@ -933,31 +1026,33 @@ const FloatingPromptInputInner = ( side="top" /> - {/* Thinking Mode Picker */} - - - - - - -

{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}

-

{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}

-
-
- - } + + + + + + +

Thinking: {THINKING_MODES.find(m => m.id === selectedThinkingMode)?.name || "Auto"}

+

{THINKING_MODES.find(m => m.id === selectedThinkingMode)?.description}

+
+
+ + } content={
{THINKING_MODES.map((mode) => ( @@ -973,7 +1068,9 @@ const FloatingPromptInputInner = ( selectedThinkingMode === mode.id && "bg-accent" )} > - + + {mode.icon} +
{mode.name} @@ -993,7 +1090,9 @@ const FloatingPromptInputInner = ( side="top" /> - {/* Prompt Input */} +
+ + {/* Prompt Input - Center */}