From dc4e630ccebc4028f227e120eede48ca0b680114 Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Fri, 27 Feb 2026 01:39:28 +0800 Subject: [PATCH 1/8] Improvements --- src-tauri/Cargo.lock | 2 +- src-tauri/capabilities/default.json | 5 +- src-tauri/src/commands/ai.rs | 20 ++- src-tauri/src/commands/claude_code.rs | 32 ++-- src-tauri/src/commands/fs.rs | 9 +- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/pty.rs | 33 +++- src-tauri/src/commands/window.rs | 16 ++ src-tauri/src/lib.rs | 75 ++++++++- src-tauri/src/services/file_watcher.rs | 33 ++-- src-tauri/src/state.rs | 10 +- src/App.tsx | 15 +- src/components/editor/MarkdownPreview.tsx | 139 +++++++++++++++ src/components/editor/Tab.tsx | 196 +++++++++++++++++----- src/components/editor/TabBar.tsx | 6 +- src/components/layout/DevPreviewPanel.tsx | 19 +++ src/components/layout/EditorArea.tsx | 5 +- src/components/layout/RightSidebar.tsx | 4 +- src/components/terminal/TerminalPanel.tsx | 19 +++ src/stores/fileStore.ts | 78 ++++++++- src/stores/gitStore.ts | 6 + src/types/files.ts | 1 + unnamed.jpg | Bin 0 -> 73510 bytes 23 files changed, 609 insertions(+), 115 deletions(-) create mode 100644 src-tauri/src/commands/window.rs create mode 100644 src/components/editor/MarkdownPreview.tsx create mode 100644 unnamed.jpg diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 57441bb..cfeb426 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -349,7 +349,7 @@ dependencies = [ [[package]] name = "clif" -version = "1.1.0" +version = "1.3.0" dependencies = [ "env_logger", "futures", diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 75e8d68..4a921d4 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,8 +1,8 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Capability for the main window", - "windows": ["main"], + "description": "Capability for all windows", + "windows": ["main", "window-*"], "permissions": [ "core:default", "shell:allow-open", @@ -17,6 +17,7 @@ "core:window:allow-maximize", "core:window:allow-set-size", "core:window:allow-set-title", + "core:window:allow-create", "core:event:default", "core:event:allow-emit", "core:event:allow-listen" diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index 47eeace..1cce745 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -1,7 +1,7 @@ use serde_json::json; use std::fs; use std::path::PathBuf; -use tauri::Emitter; +use tauri::{Emitter, Manager}; #[derive(serde::Deserialize, Clone)] pub struct ChatMessage { @@ -53,12 +53,14 @@ fn get_provider_url(provider: &str) -> String { #[tauri::command] pub async fn ai_chat( - app: tauri::AppHandle, + window: tauri::Window, messages: Vec, model: String, api_key: Option, provider: String, ) -> Result<(), String> { + let app = window.app_handle().clone(); + let label = window.label().to_string(); let url = get_provider_url(&provider); // Build the messages array for the API @@ -110,7 +112,7 @@ pub async fn ai_chat( let response = match req_builder.json(&request_body).send().await { Ok(resp) => resp, Err(e) => { - let _ = app.emit("ai_stream_error", format!("Request failed: {}", e)); + let _ = app.emit_to(&label, "ai_stream_error", format!("Request failed: {}", e)); return; } }; @@ -118,7 +120,8 @@ pub async fn ai_chat( if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); - let _ = app.emit( + let _ = app.emit_to( + &label, "ai_stream_error", format!("API error {}: {}", status, body), ); @@ -150,7 +153,7 @@ pub async fn ai_chat( let data = &line[6..]; if data == "[DONE]" { - let _ = app.emit("ai_stream", "[DONE]"); + let _ = app.emit_to(&label, "ai_stream", "[DONE]"); return; } @@ -165,7 +168,7 @@ pub async fn ai_chat( if let Some(content) = delta.get("content").and_then(|c| c.as_str()) { - let _ = app.emit("ai_stream", content); + let _ = app.emit_to(&label, "ai_stream", content); } } } @@ -175,7 +178,8 @@ pub async fn ai_chat( } } Err(e) => { - let _ = app.emit( + let _ = app.emit_to( + &label, "ai_stream_error", format!("Stream read error: {}", e), ); @@ -185,7 +189,7 @@ pub async fn ai_chat( } // If we get here without a [DONE], still signal completion - let _ = app.emit("ai_stream", "[DONE]"); + let _ = app.emit_to(&label, "ai_stream", "[DONE]"); }); Ok(()) diff --git a/src-tauri/src/commands/claude_code.rs b/src-tauri/src/commands/claude_code.rs index 20736e2..e7b62d6 100644 --- a/src-tauri/src/commands/claude_code.rs +++ b/src-tauri/src/commands/claude_code.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use tauri::Emitter; +use tauri::{Emitter, Manager}; use tokio::io::AsyncReadExt; use tokio::process::Command; use uuid::Uuid; @@ -37,12 +37,14 @@ struct ClaudeCodeEvent { #[tauri::command] pub async fn claude_code_start( - app: tauri::AppHandle, + window: tauri::Window, task: String, working_dir: String, ) -> Result { let session_id = Uuid::new_v4().to_string(); let sid = session_id.clone(); + let app = window.app_handle().clone(); + let label = window.label().to_string(); let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); @@ -53,13 +55,12 @@ pub async fn claude_code_start( sessions.insert(session_id.clone(), cancel_tx); } - let app_handle = app.clone(); - tokio::spawn(async move { - let result = run_claude_process(app_handle.clone(), &sid, &task, &working_dir, cancel_rx).await; + let result = run_claude_process(app.clone(), &label, &sid, &task, &working_dir, cancel_rx).await; if let Err(e) = result { - let _ = app_handle.emit( + let _ = app.emit_to( + &label, "claude-code-output", ClaudeCodeEvent { session_id: sid.clone(), @@ -69,7 +70,8 @@ pub async fn claude_code_start( ); } - let _ = app_handle.emit( + let _ = app.emit_to( + &label, "claude-code-output", ClaudeCodeEvent { session_id: sid.clone(), @@ -88,6 +90,7 @@ pub async fn claude_code_start( async fn run_claude_process( app: tauri::AppHandle, + label: &str, session_id: &str, task: &str, working_dir: &str, @@ -95,7 +98,6 @@ async fn run_claude_process( ) -> Result<(), String> { let claude_bin = resolve_claude_path(); - // Use --verbose and pipe through to get real-time output let mut child = Command::new(&claude_bin) .arg("-p") .arg(task) @@ -114,18 +116,19 @@ async fn run_claude_process( let sid = session_id.to_string(); let app_stdout = app.clone(); + let label_stdout = label.to_string(); let sid_stdout = sid.clone(); - // Spawn a task to read stdout in raw chunks let stdout_handle = tokio::spawn(async move { let mut reader = tokio::io::BufReader::new(stdout); let mut buf = [0u8; 4096]; loop { match reader.read(&mut buf).await { - Ok(0) => break, // EOF + Ok(0) => break, Ok(n) => { let text = String::from_utf8_lossy(&buf[..n]).to_string(); - let _ = app_stdout.emit( + let _ = app_stdout.emit_to( + &label_stdout, "claude-code-output", ClaudeCodeEvent { session_id: sid_stdout.clone(), @@ -140,9 +143,9 @@ async fn run_claude_process( }); let app_stderr = app.clone(); + let label_stderr = label.to_string(); let sid_stderr = sid.clone(); - // Spawn a task to read stderr in raw chunks let stderr_handle = tokio::spawn(async move { let mut reader = tokio::io::BufReader::new(stderr); let mut buf = [0u8; 4096]; @@ -151,7 +154,8 @@ async fn run_claude_process( Ok(0) => break, Ok(n) => { let text = String::from_utf8_lossy(&buf[..n]).to_string(); - let _ = app_stderr.emit( + let _ = app_stderr.emit_to( + &label_stderr, "claude-code-output", ClaudeCodeEvent { session_id: sid_stderr.clone(), @@ -165,7 +169,6 @@ async fn run_claude_process( } }); - // Wait for either cancellation or process completion tokio::select! { _ = &mut cancel_rx => { let _ = child.kill().await; @@ -173,7 +176,6 @@ async fn run_claude_process( _ = child.wait() => {} } - // Wait for readers to finish draining let _ = stdout_handle.await; let _ = stderr_handle.await; diff --git a/src-tauri/src/commands/fs.rs b/src-tauri/src/commands/fs.rs index 1eee7cb..a7e7192 100644 --- a/src-tauri/src/commands/fs.rs +++ b/src-tauri/src/commands/fs.rs @@ -1,7 +1,7 @@ use crate::services::file_watcher::WatcherState; -use crate::state::AppState; use std::fs; use std::path::Path; +use tauri::Manager; #[derive(serde::Serialize, Clone)] pub struct FileEntry { @@ -13,7 +13,7 @@ pub struct FileEntry { } #[tauri::command] -pub fn read_dir(path: String, _state: tauri::State<'_, AppState>) -> Result, String> { +pub fn read_dir(path: String) -> Result, String> { let dir_path = Path::new(&path); if !dir_path.exists() { @@ -170,8 +170,9 @@ pub fn delete_entry(path: String) -> Result<(), String> { #[tauri::command] pub fn watch_dir( path: String, - app: tauri::AppHandle, + window: tauri::Window, watcher_state: tauri::State<'_, WatcherState>, ) -> Result<(), String> { - crate::services::file_watcher::start_watching(&app, &watcher_state, &path) + let app = window.app_handle(); + crate::services::file_watcher::start_watching(app, &watcher_state, &path, window.label()) } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 244bc55..9f87af9 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -5,3 +5,4 @@ pub mod git; pub mod pty; pub mod search; pub mod settings; +pub mod window; diff --git a/src-tauri/src/commands/pty.rs b/src-tauri/src/commands/pty.rs index a8a63af..42edd18 100644 --- a/src-tauri/src/commands/pty.rs +++ b/src-tauri/src/commands/pty.rs @@ -13,6 +13,7 @@ struct PtySession { master: Box, child: Box, kill_flag: Arc>, + window_label: String, } impl PtyState { @@ -21,6 +22,26 @@ impl PtyState { sessions: Mutex::new(HashMap::new()), } } + + pub fn kill_all_for_window(&self, label: &str) { + if let Ok(mut sessions) = self.sessions.lock() { + let to_remove: Vec = sessions + .iter() + .filter(|(_, s)| s.window_label == label) + .map(|(id, _)| id.clone()) + .collect(); + + for id in to_remove { + if let Some(mut session) = sessions.remove(&id) { + if let Ok(mut flag) = session.kill_flag.lock() { + *flag = true; + } + let _ = session.child.kill(); + drop(session); + } + } + } + } } #[derive(Clone, serde::Serialize)] @@ -36,9 +57,11 @@ struct PtyExit { #[tauri::command] pub async fn pty_spawn( - app: AppHandle, + window: tauri::Window, working_dir: Option, ) -> Result { + let app = window.app_handle().clone(); + let label = window.label().to_string(); let session_id = uuid::Uuid::new_v4().to_string(); let pty_system = native_pty_system(); @@ -94,6 +117,7 @@ pub async fn pty_spawn( master: pair.master, child, kill_flag: kill_flag.clone(), + window_label: label.clone(), }; let state = app.state::(); @@ -106,6 +130,7 @@ pub async fn pty_spawn( // Spawn reader thread to stream output let sid = session_id.clone(); let app_clone = app.clone(); + let label_clone = label.clone(); std::thread::spawn(move || { let mut buf = [0u8; 32768]; // 32KB buffer for heavy TUI output loop { @@ -118,7 +143,8 @@ pub async fn pty_spawn( Ok(0) => break, // EOF — shell exited Ok(n) => { let data = String::from_utf8_lossy(&buf[..n]).to_string(); - let _ = app_clone.emit( + let _ = app_clone.emit_to( + &label_clone, "pty-output", PtyOutput { session_id: sid.clone(), @@ -139,7 +165,8 @@ pub async fn pty_spawn( } // Emit exit event so the frontend knows the session died - let _ = app_clone.emit( + let _ = app_clone.emit_to( + &label_clone, "pty-exit", PtyExit { session_id: sid.clone(), diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs new file mode 100644 index 0000000..37eaf5c --- /dev/null +++ b/src-tauri/src/commands/window.rs @@ -0,0 +1,16 @@ +#[tauri::command] +pub async fn create_window(app: tauri::AppHandle) -> Result { + let label = format!("window-{}", &uuid::Uuid::new_v4().to_string()[..8]); + + tauri::WebviewWindowBuilder::new( + &app, + &label, + tauri::WebviewUrl::App("index.html".into()), + ) + .title("Clif") + .inner_size(1400.0, 900.0) + .build() + .map_err(|e| format!("Failed to create window: {}", e))?; + + Ok(label) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9d6dafe..dc176e1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,21 +4,79 @@ mod state; use commands::pty::PtyState; use services::file_watcher::WatcherState; -use state::AppState; -use std::sync::Mutex; +use tauri::Manager; -pub fn run() { - let app_state = AppState { - project_root: Mutex::new(None), - open_files: Mutex::new(std::collections::HashMap::new()), - }; +fn build_menu(app: &tauri::AppHandle) -> Result, tauri::Error> { + use tauri::menu::{MenuBuilder, SubmenuBuilder, PredefinedMenuItem, MenuItemBuilder}; + + let new_window = MenuItemBuilder::with_id("new_window", "New Window") + .accelerator("CmdOrCtrl+Shift+N") + .build(app)?; + + let file_menu = SubmenuBuilder::new(app, "File") + .item(&new_window) + .separator() + .item(&PredefinedMenuItem::close_window(app, None)?) + .item(&PredefinedMenuItem::quit(app, None)?) + .build()?; + + let edit_menu = SubmenuBuilder::new(app, "Edit") + .item(&PredefinedMenuItem::undo(app, None)?) + .item(&PredefinedMenuItem::redo(app, None)?) + .separator() + .item(&PredefinedMenuItem::cut(app, None)?) + .item(&PredefinedMenuItem::copy(app, None)?) + .item(&PredefinedMenuItem::paste(app, None)?) + .item(&PredefinedMenuItem::select_all(app, None)?) + .build()?; + + let window_menu = SubmenuBuilder::new(app, "Window") + .item(&PredefinedMenuItem::minimize(app, None)?) + .item(&PredefinedMenuItem::maximize(app, None)?) + .build()?; + MenuBuilder::new(app) + .item(&file_menu) + .item(&edit_menu) + .item(&window_menu) + .build() +} + +pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) - .manage(app_state) .manage(PtyState::new()) .manage(WatcherState::new()) + .setup(|app| { + let menu = build_menu(app.handle())?; + app.set_menu(menu)?; + Ok(()) + }) + .on_menu_event(|app, event| { + if event.id().as_ref() == "new_window" { + let app = app.clone(); + tauri::async_runtime::spawn(async move { + let _ = commands::window::create_window(app).await; + }); + } + }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::Destroyed = event { + let label = window.label().to_string(); + let app = window.app_handle(); + + // Clean up PTY sessions for this window + if let Some(pty_state) = app.try_state::() { + pty_state.kill_all_for_window(&label); + } + + // Clean up file watchers for this window + if let Some(watcher_state) = app.try_state::() { + services::file_watcher::stop_all_for_window(&watcher_state, &label); + } + } + }) .invoke_handler(tauri::generate_handler![ commands::fs::read_dir, commands::fs::read_file, @@ -54,6 +112,7 @@ pub fn run() { commands::pty::pty_write, commands::pty::pty_resize, commands::pty::pty_kill, + commands::window::create_window, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/services/file_watcher.rs b/src-tauri/src/services/file_watcher.rs index 3fa93d8..22b50ec 100644 --- a/src-tauri/src/services/file_watcher.rs +++ b/src-tauri/src/services/file_watcher.rs @@ -1,6 +1,6 @@ use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use std::collections::HashMap; use std::path::PathBuf; -use std::sync::mpsc; use std::sync::Mutex; use tauri::{AppHandle, Emitter}; @@ -11,7 +11,7 @@ pub struct FileChangeEvent { } pub struct WatcherState { - watcher: Mutex>, + watchers: Mutex>, } struct WatcherHandle { @@ -21,7 +21,7 @@ struct WatcherHandle { impl WatcherState { pub fn new() -> Self { WatcherState { - watcher: Mutex::new(None), + watchers: Mutex::new(HashMap::new()), } } } @@ -30,11 +30,12 @@ pub fn start_watching( app: &AppHandle, state: &WatcherState, path: &str, + window_label: &str, ) -> Result<(), String> { - // Stop any existing watcher - stop_watching(state); + // Stop any existing watcher for this window + stop_watching(state, window_label); - let (tx, rx) = mpsc::channel::>(); + let (tx, rx) = std::sync::mpsc::channel::>(); let mut watcher = RecommendedWatcher::new(tx, Config::default()) .map_err(|e| format!("Failed to create watcher: {}", e))?; @@ -45,6 +46,7 @@ pub fn start_watching( let app_clone = app.clone(); let watch_path = path.to_string(); + let label = window_label.to_string(); // Spawn thread to process file system events std::thread::spawn(move || { @@ -67,7 +69,8 @@ pub fn start_watching( // Only emit for actual files, not directories if path.is_file() || kind_str == "remove" { - let _ = app_clone.emit( + let _ = app_clone.emit_to( + &label, "file-changed", FileChangeEvent { path: path_str, @@ -80,19 +83,23 @@ pub fn start_watching( } }); - let mut guard = state.watcher.lock().map_err(|e| format!("Lock error: {}", e))?; - *guard = Some(WatcherHandle { _watcher: watcher }); + let mut guard = state.watchers.lock().map_err(|e| format!("Lock error: {}", e))?; + guard.insert(window_label.to_string(), WatcherHandle { _watcher: watcher }); - log::info!("File watcher started for: {}", watch_path); + log::info!("File watcher started for: {} (window: {})", watch_path, window_label); Ok(()) } -pub fn stop_watching(state: &WatcherState) { - if let Ok(mut guard) = state.watcher.lock() { - *guard = None; +pub fn stop_watching(state: &WatcherState, window_label: &str) { + if let Ok(mut guard) = state.watchers.lock() { + guard.remove(window_label); } } +pub fn stop_all_for_window(state: &WatcherState, window_label: &str) { + stop_watching(state, window_label); +} + fn should_ignore(path: &str) -> bool { let ignore_patterns = [ "/node_modules/", diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 24a1166..560ffa0 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,8 +1,2 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Mutex; - -pub struct AppState { - pub project_root: Mutex>, - pub open_files: Mutex>, -} +// App state is managed per-window via Tauri's built-in state management. +// PTY sessions and file watchers are tracked in their respective state structs. diff --git a/src/App.tsx b/src/App.tsx index ad2ee50..cf05b20 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { Component, onMount, Show, createSignal, lazy, Suspense } from "solid-js"; +import { Component, onMount, createEffect, Show, createSignal, lazy, Suspense } from "solid-js"; import TopBar from "./components/layout/TopBar"; import EditorArea from "./components/layout/EditorArea"; import StatusBar from "./components/layout/StatusBar"; @@ -84,6 +84,17 @@ const App: Component = () => { document.addEventListener("mouseup", onMouseUp); } + createEffect(async () => { + const root = projectRoot(); + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + if (root) { + const folder = root.split("/").pop() || root; + getCurrentWindow().setTitle(`${folder} — Clif`); + } else { + getCurrentWindow().setTitle("Clif"); + } + }); + onMount(async () => { configureMonaco(); @@ -126,7 +137,7 @@ const App: Component = () => { } > - (terminalRef = r)} /> + (terminalRef = r)} workingDir={projectRoot() || undefined} /> diff --git a/src/components/editor/MarkdownPreview.tsx b/src/components/editor/MarkdownPreview.tsx new file mode 100644 index 0000000..043c499 --- /dev/null +++ b/src/components/editor/MarkdownPreview.tsx @@ -0,0 +1,139 @@ +import { Component, createMemo } from "solid-js"; +import { marked } from "marked"; +import { activeFile } from "../../stores/fileStore"; + +marked.setOptions({ async: false, breaks: true, gfm: true }); + +const MarkdownPreview: Component = () => { + const html = createMemo(() => marked.parse(activeFile()?.content ?? "") as string); + + return ( +
+
+ + +
+ ); +}; + +export default MarkdownPreview; diff --git a/src/components/editor/Tab.tsx b/src/components/editor/Tab.tsx index 3e5f934..965bd74 100644 --- a/src/components/editor/Tab.tsx +++ b/src/components/editor/Tab.tsx @@ -1,4 +1,5 @@ -import { Component, Show } from "solid-js"; +import { Component, Show, createSignal, onCleanup } from "solid-js"; +import { Portal } from "solid-js/web"; import type { OpenFile } from "../../types/files"; interface TabProps { @@ -6,6 +7,10 @@ interface TabProps { isActive: boolean; onSelect: () => void; onClose: () => void; + onCloseOthers: () => void; + onCloseAll: () => void; + onCloseToRight: () => void; + onPreview?: () => void; } function getExtensionColor(name: string): string { @@ -42,58 +47,159 @@ function getExtensionColor(name: string): string { } const Tab: Component = (props) => { - return ( -
props.onSelect()} - onMouseDown={(e) => { - // Middle click to close - if (e.button === 1) { - e.preventDefault(); - props.onClose(); - } - }} - > - {/* File extension color dot */} - + const [showMenu, setShowMenu] = createSignal(false); + const [menuPos, setMenuPos] = createSignal({ x: 0, y: 0 }); - {/* Dirty indicator */} - - - + const handleContextMenu = (e: MouseEvent) => { + e.preventDefault(); + setMenuPos({ x: e.clientX, y: e.clientY }); + setShowMenu(true); - {/* File name */} - - {props.file.name} - + const closeMenu = (ev: MouseEvent) => { + if (!(ev.target as HTMLElement).closest("[data-tab-menu]")) { + setShowMenu(false); + document.removeEventListener("mousedown", closeMenu); + } + }; + document.addEventListener("mousedown", closeMenu); + onCleanup(() => document.removeEventListener("mousedown", closeMenu)); + }; - {/* Close button */} - + stroke-linejoin="round" + > + + + + + + {/* Dirty indicator */} + + + + + {/* File name */} + + {props.file.name} + + + {/* Close button */} + +
+ + {/* Context menu (portaled to body to avoid overflow clipping) */} + + +
+ + + + + +
+ + +
+ +
); }; diff --git a/src/components/editor/TabBar.tsx b/src/components/editor/TabBar.tsx index cc97eab..09aa381 100644 --- a/src/components/editor/TabBar.tsx +++ b/src/components/editor/TabBar.tsx @@ -1,5 +1,5 @@ import { Component, For, Show } from "solid-js"; -import { openFiles, activeFilePath, setActiveFilePath, closeFile } from "../../stores/fileStore"; +import { openFiles, activeFilePath, setActiveFilePath, closeFile, closeOtherFiles, closeAllFiles, closeFilesToRight, openPreview } from "../../stores/fileStore"; import Tab from "./Tab"; const TabBar: Component = () => { @@ -16,6 +16,10 @@ const TabBar: Component = () => { isActive={activeFilePath() === file.path} onSelect={() => setActiveFilePath(file.path)} onClose={() => closeFile(file.path)} + onCloseOthers={() => closeOtherFiles(file.path)} + onCloseAll={() => closeAllFiles()} + onCloseToRight={() => closeFilesToRight(file.path)} + onPreview={!file.isPreview && file.name.endsWith(".md") ? () => openPreview(file.path) : undefined} /> )} diff --git a/src/components/layout/DevPreviewPanel.tsx b/src/components/layout/DevPreviewPanel.tsx index 246e00e..e11fbdb 100644 --- a/src/components/layout/DevPreviewPanel.tsx +++ b/src/components/layout/DevPreviewPanel.tsx @@ -234,6 +234,25 @@ const DevPreviewPanel: Component = () => { } }); + // Respawn session when project root changes + let prevRoot: string | null | undefined = undefined; + createEffect(() => { + const root = projectRoot(); + if (prevRoot !== undefined && root && root !== prevRoot) { + const sid = sessionId(); + if (sid && terminalMounted) { + ptyKill(sid).catch(() => {}); + unlistenOutput?.(); + unlistenExit?.(); + dataDisposable?.dispose(); + setSessionId(null); + terminal?.clear(); + spawnSession(); + } + } + prevRoot = root; + }); + // Watch theme changes createEffect(() => { const t = theme(); diff --git a/src/components/layout/EditorArea.tsx b/src/components/layout/EditorArea.tsx index b5f5989..99a517d 100644 --- a/src/components/layout/EditorArea.tsx +++ b/src/components/layout/EditorArea.tsx @@ -3,6 +3,7 @@ import { activeFile, openFiles } from "../../stores/fileStore"; const TabBar = lazy(() => import("../editor/TabBar")); const MonacoEditor = lazy(() => import("../editor/MonacoEditor")); +const MarkdownPreview = lazy(() => import("../editor/MarkdownPreview")); const EmptyState: Component = () => (
{
} > - + }> + +
diff --git a/src/components/layout/RightSidebar.tsx b/src/components/layout/RightSidebar.tsx index b328ee7..324c624 100644 --- a/src/components/layout/RightSidebar.tsx +++ b/src/components/layout/RightSidebar.tsx @@ -3,7 +3,7 @@ import { projectRoot, openFile } from "../../stores/fileStore"; import { isGitRepo, currentBranch, changedFiles, diffStat, stagedFiles, unstagedFiles, commitLog, fileNumstats, - refreshGitStatus, stageFile, unstageFile, stageAll, unstageAll, commitChanges, initializeRepo, + refreshGitStatus, refreshBranches, stageFile, unstageFile, stageAll, unstageAll, commitChanges, initializeRepo, } from "../../stores/gitStore"; import { devDrawerOpen, devDrawerHeight, setDevDrawerHeight } from "../../stores/uiStore"; import type { GitLogEntry } from "../../types/git"; @@ -612,7 +612,7 @@ const RightSidebar: Component<{ onOpenFolder?: () => void }> = (props) => { onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = "transparent"; }} - onClick={() => refreshGitStatus()} + onClick={() => { refreshGitStatus(); refreshBranches(); }} > Refresh diff --git a/src/components/terminal/TerminalPanel.tsx b/src/components/terminal/TerminalPanel.tsx index b5badf2..5ba8e22 100644 --- a/src/components/terminal/TerminalPanel.tsx +++ b/src/components/terminal/TerminalPanel.tsx @@ -247,6 +247,25 @@ const TerminalPanel: Component<{ ref?: (ref: TerminalPanelRef) => void; workingD } }); + // Respawn session when workingDir changes + let prevWorkingDir: string | undefined = undefined; + createEffect(() => { + const dir = props.workingDir; + if (prevWorkingDir !== undefined && dir && dir !== prevWorkingDir) { + const sid = sessionId(); + if (sid && terminal) { + ptyKill(sid).catch(() => {}); + unlistenOutput?.(); + unlistenExit?.(); + dataDisposable?.dispose(); + setSessionId(null); + terminal.clear(); + spawnSession(dir); + } + } + prevWorkingDir = dir; + }); + onCleanup(async () => { alive = false; resizeObserver?.disconnect(); diff --git a/src/stores/fileStore.ts b/src/stores/fileStore.ts index ba4ba05..46d94ce 100644 --- a/src/stores/fileStore.ts +++ b/src/stores/fileStore.ts @@ -98,10 +98,11 @@ async function openProject(path: string) { const entries = await loadDirectory(r); setFileTree(entries); } - // Refresh git status too + // Refresh git status and branches try { - const { refreshGitStatus } = await import("./gitStore"); + const { refreshGitStatus, refreshBranches } = await import("./gitStore"); refreshGitStatus(); + refreshBranches(); } catch {} }, 500); }); @@ -168,6 +169,41 @@ function closeFile(path: string) { } } +function closeOtherFiles(path: string) { + setOpenFiles( + produce((files) => { + for (let i = files.length - 1; i >= 0; i--) { + if (files[i].path !== path) files.splice(i, 1); + } + }) + ); + setActiveFilePath(path); +} + +function closeAllFiles() { + setOpenFiles( + produce((files) => { + files.splice(0, files.length); + }) + ); + setActiveFilePath(null); +} + +function closeFilesToRight(path: string) { + const idx = openFiles.findIndex((f) => f.path === path); + if (idx === -1) return; + setOpenFiles( + produce((files) => { + files.splice(idx + 1); + }) + ); + // If the active file was to the right, switch to the given file + const activePath = activeFilePath(); + if (activePath && !openFiles.find((f) => f.path === activePath)) { + setActiveFilePath(path); + } +} + function updateFileContent(path: string, content: string) { const idx = openFiles.findIndex((f) => f.path === path); if (idx === -1) return; @@ -193,6 +229,40 @@ async function saveActiveFile() { if (path) await saveFile(path); } +async function openPreview(sourcePath: string) { + const previewPath = sourcePath + "::preview"; + + // If preview already open, switch to it + const existing = openFiles.find((f) => f.path === previewPath); + if (existing) { + setActiveFilePath(previewPath); + return; + } + + // Get content from already-open source file or read from disk + let content: string; + const sourceFile = openFiles.find((f) => f.path === sourcePath); + if (sourceFile) { + content = sourceFile.content; + } else { + try { + content = await readFile(sourcePath); + } catch (e) { + console.error("Failed to read file for preview:", e); + return; + } + } + + const name = "Preview: " + getFileName(sourcePath); + + setOpenFiles( + produce((files) => { + files.push({ path: previewPath, name, content, language: "markdown", isDirty: false, isPreview: true }); + }) + ); + setActiveFilePath(previewPath); +} + export { projectRoot, setProjectRoot, @@ -213,4 +283,8 @@ export { updateFileContent, saveFile, saveActiveFile, + openPreview, + closeOtherFiles, + closeAllFiles, + closeFilesToRight, }; diff --git a/src/stores/gitStore.ts b/src/stores/gitStore.ts index fb2f1dc..7b770c9 100644 --- a/src/stores/gitStore.ts +++ b/src/stores/gitStore.ts @@ -124,9 +124,15 @@ async function initializeRepo() { await refreshBranches(); } +let branchPollTimer: ReturnType | undefined; + async function initGit() { await refreshGitStatus(); await refreshBranches(); + + // Poll for branch changes from external tools (e.g. CLI git checkout) + if (branchPollTimer) clearInterval(branchPollTimer); + branchPollTimer = setInterval(refreshBranches, 3000); } export { diff --git a/src/types/files.ts b/src/types/files.ts index 5ce0296..d6afe7e 100644 --- a/src/types/files.ts +++ b/src/types/files.ts @@ -12,4 +12,5 @@ export interface OpenFile { content: string; language: string; isDirty: boolean; + isPreview?: boolean; } diff --git a/unnamed.jpg b/unnamed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6e8849b242f18897c15e90b349a022b2928c0096 GIT binary patch literal 73510 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%0Dn3Ge^`A`b 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 literal 0 HcmV?d00001 From 7c420a0f1c429e7b212b2c55338c3625ba46c6b9 Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Sun, 1 Mar 2026 13:11:05 +0800 Subject: [PATCH 2/8] feat: implement FIM ghost text completions and add ClifCode launch button Add inline completion provider using Fill-in-Middle format for AI-powered ghost text suggestions, and add a ClifCode button to the top bar for launching the offline AI assistant from the terminal. --- src-tauri/src/commands/ai.rs | 4 +- src/App.tsx | 8 +- src/components/editor/GhostText.tsx | 119 +++++++++++++++++++++---- src/components/editor/MonacoEditor.tsx | 9 ++ src/components/layout/TopBar.tsx | 42 +++++++++ 5 files changed, 162 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index 1cce745..310312b 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -209,11 +209,11 @@ pub async fn ai_complete( "messages": [ { "role": "system", - "content": "You are a code completion assistant. Given the code context, provide only the completion text. Do not include explanations, markdown formatting, or code fences. Output only the raw code that should be inserted." + "content": "You are a code completion assistant. The user provides code with <|fim_prefix|> before the cursor and <|fim_suffix|> after. Output ONLY the code to insert at the cursor. No explanations, no markdown, no fences." }, { "role": "user", - "content": format!("Complete the following code:\n\n{}", context) + "content": context } ], "max_tokens": 256, diff --git a/src/App.tsx b/src/App.tsx index cf05b20..0e39dea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,12 @@ const App: Component = () => { } } + function handleLaunchClifCode() { + if (terminalRef && projectRoot()) { + terminalRef.sendCommand("clifcode\n"); + } + } + async function handleOpenFolder() { try { const { open } = await import("@tauri-apps/plugin-dialog"); @@ -117,7 +123,7 @@ const App: Component = () => { style={{ background: "var(--bg-base)", color: "var(--text-primary)" }} > {/* Top Bar */} - + {/* Main content: Terminal (left) + Editor (center) + Sidebar (right) */}
diff --git a/src/components/editor/GhostText.tsx b/src/components/editor/GhostText.tsx index 1a125cc..c13b853 100644 --- a/src/components/editor/GhostText.tsx +++ b/src/components/editor/GhostText.tsx @@ -1,21 +1,106 @@ -import { Component } from "solid-js"; import * as monaco from "monaco-editor"; +import { settings } from "../../stores/settingsStore"; +import { aiComplete, getApiKey } from "../../lib/tauri"; -export function registerGhostTextProvider(editor: monaco.editor.IStandaloneCodeEditor) { - // Will be implemented in Phase 2 - // This will register an InlineCompletionsProvider with Monaco - // that provides AI-powered ghost text completions as the user types. - // - // The provider will: - // 1. Detect when the user pauses typing - // 2. Send context to the AI backend (OpenRouter/Ollama) - // 3. Return inline completion items for Monaco to render as ghost text - // 4. Handle accept (Tab), dismiss (Escape), and partial accept - void editor; -} +/** + * Register an inline completions provider that fetches FIM ghost text + * from the configured AI backend. Returns a disposable to tear down + * the provider when the editor unmounts. + */ +export function registerGhostTextProvider( + editor: monaco.editor.IStandaloneCodeEditor +): monaco.IDisposable { + let debounceTimer: ReturnType | null = null; + + const provider = monaco.languages.registerInlineCompletionsProvider("*", { + provideInlineCompletions: async (model, position, _ctx, token) => { + // Cancel any pending debounce + if (debounceTimer) { + clearTimeout(debounceTimer); + debounceTimer = null; + } + + // 500ms debounce — wait for the user to pause typing + const completion = await new Promise((resolve) => { + debounceTimer = setTimeout(async () => { + if (token.isCancellationRequested) { + resolve(null); + return; + } + + try { + // Extract prefix (~1500 chars before cursor) and suffix (~500 chars after) + const fullText = model.getValue(); + const offset = model.getOffsetAt(position); + const prefix = fullText.slice(Math.max(0, offset - 1500), offset); + const suffix = fullText.slice(offset, offset + 500); + + // Format as FIM prompt + const context = `<|fim_prefix|>${prefix}<|fim_suffix|>${suffix}<|fim_middle|>`; + + const apiKey = await getApiKey(settings().aiProvider); + const result = await aiComplete( + context, + settings().aiModel, + apiKey, + settings().aiProvider + ); + + if (token.isCancellationRequested) { + resolve(null); + return; + } -const GhostText: Component = () => { - return null; -}; + // Strip markdown fences if the model wrapped its output + let cleaned = result.trim(); + if (cleaned.startsWith("```")) { + const firstNewline = cleaned.indexOf("\n"); + const lastFence = cleaned.lastIndexOf("```"); + if (firstNewline !== -1 && lastFence > firstNewline) { + cleaned = cleaned.slice(firstNewline + 1, lastFence).trim(); + } + } -export default GhostText; + resolve(cleaned || null); + } catch { + resolve(null); + } + }, 500); + }); + + if (!completion || token.isCancellationRequested) { + return { items: [] }; + } + + const range = new monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ); + + return { + items: [ + { + insertText: completion, + range, + }, + ], + }; + }, + + freeInlineCompletions() { + // no-op — nothing to free + }, + }); + + // Clean up debounce timer when provider is disposed + return { + dispose() { + if (debounceTimer) { + clearTimeout(debounceTimer); + } + provider.dispose(); + }, + }; +} diff --git a/src/components/editor/MonacoEditor.tsx b/src/components/editor/MonacoEditor.tsx index 84c350f..d65522f 100644 --- a/src/components/editor/MonacoEditor.tsx +++ b/src/components/editor/MonacoEditor.tsx @@ -3,6 +3,7 @@ import * as monaco from "monaco-editor"; import { activeFile, updateFileContent, saveActiveFile } from "../../stores/fileStore"; import { theme, fontSize } from "../../stores/uiStore"; import { monacoThemes } from "../../lib/themes"; +import { registerGhostTextProvider } from "./GhostText"; import type { Theme } from "../../stores/uiStore"; // Model + viewstate cache @@ -41,6 +42,7 @@ function getMonacoThemeName(t: Theme): string { const MonacoEditor: Component = () => { let containerRef!: HTMLDivElement; let editorInstance: monaco.editor.IStandaloneCodeEditor | undefined; + let ghostTextDisposable: monaco.IDisposable | undefined; let currentPath: string | null = null; let onChangeDisposable: monaco.IDisposable | undefined; @@ -70,6 +72,9 @@ const MonacoEditor: Component = () => { theme: getMonacoThemeName(theme()), }); + // Register FIM ghost text completions + ghostTextDisposable = registerGhostTextProvider(editorInstance); + // Register Cmd+S / Ctrl+S to save editorInstance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { saveActiveFile(); @@ -140,6 +145,10 @@ const MonacoEditor: Component = () => { } } + if (ghostTextDisposable) { + ghostTextDisposable.dispose(); + } + if (onChangeDisposable) { onChangeDisposable.dispose(); } diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 7f1dcfe..7068412 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -29,8 +29,16 @@ function getProjectName(): string { return parts[parts.length - 1] || ""; } +const ClifCodeIcon = () => ( + + + + +); + const TopBar: Component<{ onLaunchClaude: () => void; + onLaunchClifCode: () => void; onOpenFolder: () => void; }> = (props) => { const hasProject = () => !!projectRoot(); @@ -263,6 +271,40 @@ const TopBar: Component<{ {/* Divider */}
+ {/* Launch ClifCode button */} + + {/* Launch Claude button */} +
+ diff --git a/src/components/layout/StatusBar.tsx b/clif-pad-ide/src/components/layout/StatusBar.tsx similarity index 100% rename from src/components/layout/StatusBar.tsx rename to clif-pad-ide/src/components/layout/StatusBar.tsx diff --git a/src/components/layout/TopBar.tsx b/clif-pad-ide/src/components/layout/TopBar.tsx similarity index 100% rename from src/components/layout/TopBar.tsx rename to clif-pad-ide/src/components/layout/TopBar.tsx diff --git a/src/components/terminal/TerminalPanel.tsx b/clif-pad-ide/src/components/terminal/TerminalPanel.tsx similarity index 100% rename from src/components/terminal/TerminalPanel.tsx rename to clif-pad-ide/src/components/terminal/TerminalPanel.tsx diff --git a/src/lib/keybindings.ts b/clif-pad-ide/src/lib/keybindings.ts similarity index 100% rename from src/lib/keybindings.ts rename to clif-pad-ide/src/lib/keybindings.ts diff --git a/src/lib/monaco-setup.ts b/clif-pad-ide/src/lib/monaco-setup.ts similarity index 100% rename from src/lib/monaco-setup.ts rename to clif-pad-ide/src/lib/monaco-setup.ts diff --git a/src/lib/tauri.ts b/clif-pad-ide/src/lib/tauri.ts similarity index 100% rename from src/lib/tauri.ts rename to clif-pad-ide/src/lib/tauri.ts diff --git a/src/lib/themes.ts b/clif-pad-ide/src/lib/themes.ts similarity index 100% rename from src/lib/themes.ts rename to clif-pad-ide/src/lib/themes.ts diff --git a/src/lib/utils.ts b/clif-pad-ide/src/lib/utils.ts similarity index 100% rename from src/lib/utils.ts rename to clif-pad-ide/src/lib/utils.ts diff --git a/src/main.tsx b/clif-pad-ide/src/main.tsx similarity index 100% rename from src/main.tsx rename to clif-pad-ide/src/main.tsx diff --git a/src/stores/fileStore.ts b/clif-pad-ide/src/stores/fileStore.ts similarity index 100% rename from src/stores/fileStore.ts rename to clif-pad-ide/src/stores/fileStore.ts diff --git a/src/stores/gitStore.ts b/clif-pad-ide/src/stores/gitStore.ts similarity index 100% rename from src/stores/gitStore.ts rename to clif-pad-ide/src/stores/gitStore.ts diff --git a/src/stores/settingsStore.ts b/clif-pad-ide/src/stores/settingsStore.ts similarity index 100% rename from src/stores/settingsStore.ts rename to clif-pad-ide/src/stores/settingsStore.ts diff --git a/src/stores/uiStore.ts b/clif-pad-ide/src/stores/uiStore.ts similarity index 100% rename from src/stores/uiStore.ts rename to clif-pad-ide/src/stores/uiStore.ts diff --git a/src/styles/global.css b/clif-pad-ide/src/styles/global.css similarity index 96% rename from src/styles/global.css rename to clif-pad-ide/src/styles/global.css index ad9a515..752b11c 100644 --- a/src/styles/global.css +++ b/clif-pad-ide/src/styles/global.css @@ -246,6 +246,15 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); } + /* ─── Git graph row hover ─── */ + .git-graph-row:hover { + background: var(--bg-hover); + } + + .git-graph-row:hover .git-graph-tooltip { + max-height: 80px !important; + } + /* ─── Monaco editor overrides ─── */ .monaco-editor .overflow-guard { border-radius: 0 !important; diff --git a/src/types/ai.ts b/clif-pad-ide/src/types/ai.ts similarity index 100% rename from src/types/ai.ts rename to clif-pad-ide/src/types/ai.ts diff --git a/src/types/files.ts b/clif-pad-ide/src/types/files.ts similarity index 100% rename from src/types/files.ts rename to clif-pad-ide/src/types/files.ts diff --git a/src/types/git.ts b/clif-pad-ide/src/types/git.ts similarity index 100% rename from src/types/git.ts rename to clif-pad-ide/src/types/git.ts diff --git a/tsconfig.json b/clif-pad-ide/tsconfig.json similarity index 100% rename from tsconfig.json rename to clif-pad-ide/tsconfig.json diff --git a/tsconfig.node.json b/clif-pad-ide/tsconfig.node.json similarity index 100% rename from tsconfig.node.json rename to clif-pad-ide/tsconfig.node.json diff --git a/vercel.json b/clif-pad-ide/vercel.json similarity index 100% rename from vercel.json rename to clif-pad-ide/vercel.json diff --git a/vite.config.ts b/clif-pad-ide/vite.config.ts similarity index 100% rename from vite.config.ts rename to clif-pad-ide/vite.config.ts diff --git a/www/index.html b/clif-pad-ide/www/index.html similarity index 80% rename from www/index.html rename to clif-pad-ide/www/index.html index 70c6178..aabb448 100644 --- a/www/index.html +++ b/clif-pad-ide/www/index.html @@ -3,10 +3,10 @@ - Clif — AI-native code editor - - - + Clif — AI-native code editor & terminal agent + + + @@ -169,6 +169,32 @@ padding: 2px 8px; border-radius: 4px; margin-bottom: 12px; } + /* CLIFCODE SECTION */ + .clifcode-section { + padding: 80px 0; text-align: center; + border-top: 1px solid rgba(255,255,255,0.06); + } + .clifcode-section h2 { + font-size: 40px; font-weight: 800; letter-spacing: -1px; margin-bottom: 12px; + } + .clifcode-section .sub { color: var(--muted); font-size: 16px; margin-bottom: 32px; } + .clifcode-install { + display: inline-block; text-align: left; background: var(--bg2); + border: 1px solid rgba(255,255,255,0.06); border-radius: 12px; + padding: 20px 28px; font-family: 'JetBrains Mono', monospace; font-size: 15px; + line-height: 1.8; margin-bottom: 32px; + } + .clifcode-features { + display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; + background: rgba(255,255,255,0.04); border-radius: 16px; overflow: hidden; + margin-top: 40px; + } + .clifcode-feat { + padding: 28px 20px; background: var(--bg2); text-align: center; + } + .clifcode-feat h4 { font-size: 14px; font-weight: 700; margin-bottom: 6px; } + .clifcode-feat p { font-size: 13px; color: var(--muted); line-height: 1.5; } + /* SOURCE */ .source { padding: 60px 0 80px; text-align: center; @@ -205,6 +231,7 @@ .feat:first-child { border-radius: 16px 16px 2px 2px !important; } .feat:last-child { border-radius: 2px 2px 16px 16px !important; } .downloads { flex-direction: column; align-items: center; } + .clifcode-features { grid-template-columns: 1fr; } } @@ -217,8 +244,9 @@ @@ -350,6 +378,33 @@

Privacy first

+ +
+
+

ClifCode

+

AI coding agent for your terminal. Tool-calling loop, streaming markdown, session persistence. Works with any API.

+ +
+ $ npm i -g clifcode +
+ +
+
+

Agent loop

+

Read/write files, run commands, search code, git ops — all via tool calls

+
+
+

Any provider

+

OpenRouter, OpenAI, Anthropic, Ollama, or any OpenAI-compatible API

+
+
+

Sessions

+

Auto-saves context, resume previous sessions, per-turn cost tracking

+
+
+
+
+
@@ -369,7 +424,7 @@

FAQ

Is Clif safe?

-

100% open source. Read every line on GitHub. No telemetry, no network calls unless you enable AI. The xattr command just removes Apple's download flag — it doesn't disable any security.

+

100% open source. Read every line on GitHub. No telemetry, no network calls unless you enable AI. The xattr command just removes Apple's download flag — it doesn't disable any security.

Why isn't it signed?

@@ -387,11 +442,16 @@

Does it work offline?

Or build it yourself.

-

Three commands. Under a minute.

+

Clone the monorepo and pick your product.

- $ git clone https://github.com/DLhugly/Clif.git && cd Clif
- $ npm install
- $ npm run tauri dev + # Clone
+ $ git clone https://github.com/DLhugly/Clif-Code.git && cd Clif-Code
+
+ # ClifPad (desktop IDE)
+ $ cd clif-pad-ide && npm install && npm run tauri dev
+
+ # ClifCode (terminal agent)
+ $ cd clif-code-tui && cargo run --release
@@ -400,7 +460,7 @@

Or build it yourself.

From 7e7eacc9342a830c3353e9c8e11fe43e45d19067 Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Sun, 1 Mar 2026 18:08:13 +0800 Subject: [PATCH 4/8] docs: epic README with ASCII art, feature deep-dive for both products --- README.md | 203 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 159 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index e492a7d..8519a55 100644 --- a/README.md +++ b/README.md @@ -2,79 +2,157 @@ Clif-Code

-

Clif-Code

+
+   _____ _ _  __ _____          _
+  / ____| (_)/ _/ ____|        | |
+ | |    | |_| || |     ___   __| | ___
+ | |    | | |  _| |    / _ \ / _` |/ _ \
+ | |____| | | | | |___| (_) | (_| |  __/
+  \_____|_|_|_|  \_____\___/ \__,_|\___|
+
+ +

Two tools. One mission. Ship faster.

- ClifPad: ~20MB desktop IDE. ClifCode: terminal AI agent. + ClifPad — ~20MB desktop IDE that replaces Electron bloat
+ ClifCode — AI agent that lives in your terminal

MIT License Release + npm Platform

Website · - ClifPad · - ClifCode · - Development + ClifPad · + ClifCode · + Get Started · + Downloads

--- -## Monorepo +
+ +## Why Clif? + +Every other "AI editor" ships a 400MB Electron wrapper, locks you into a subscription, and phones home with your code. + +Clif is different: + +- **Native.** Rust backend, ~20MB binary. Launches in under a second. +- **Private.** Zero telemetry. Zero cloud sync. Your code never leaves your machine. +- **Open.** MIT licensed. Read every line. Fork it. Ship it. No strings. +- **AI on your terms.** Bring your own key — OpenRouter, OpenAI, Anthropic, Ollama. Or turn it all off and use a pure offline editor. + +
``` Clif-Code/ -├── clif-pad-ide/ # Desktop IDE (Tauri 2 + SolidJS + Monaco) -├── clif-code-tui/ # TUI terminal agent (Rust, API-only) -└── .github/ # CI/CD +├── clif-pad-ide/ Desktop IDE — Tauri 2 + SolidJS + Monaco +├── clif-code-tui/ Terminal AI agent — pure Rust, any API +└── .github/ CI/CD (auto-release, npm publish) ``` +
+ --- -## ClifPad +
-A blazing-fast, privacy-first, open-source AI-native code editor. ~20MB native binary, 7KB SolidJS frontend. +## ClifPad — The IDE -**Tech**: Tauri 2 (Rust) + SolidJS + Monaco Editor + Tailwind CSS 4 +> VS Code features. Fraction of the size. No Electron in sight. -### Features + + + + + +
-- **Monaco Editor** — 70+ languages, IntelliSense, multi-cursor, minimap, code folding -- **Real Terminal** — Native PTY via Rust, 256-color, resize, 10K scrollback -- **Dev Preview** — One-click dev server with live iframe preview -- **Git** — Branch, status, stage, commit, diff stats, visual commit graph -- **AI** — OpenRouter, Ollama (local), Claude Code CLI — all opt-in -- **5 Themes** — Midnight, Graphite, Dawn, Arctic, Dusk +**Monaco Editor** — The same engine behind VS Code. 70+ languages, IntelliSense, multi-cursor, minimap, bracket matching, code folding. Full editing power in a native shell. -### Install +**Real Terminal** — Not a web simulation. Native PTY sessions via Rust with 256-color, auto-resize, and 10K scrollback. Your actual shell, running at full speed. -Download from [Releases](https://github.com/DLhugly/Clif-Code/releases) or build from source: +**Dev Preview** — One-click dev server launcher. Auto-detects localhost ports and renders a live iframe preview. Build and see changes without leaving the editor. -```bash -cd clif-pad-ide -npm install && npm run tauri dev -``` + + +**Git Built In** — Branch, stage, commit, per-file diff stats, visual commit graph. All powered by Rust — not shell commands piped through a webview. -> **macOS "App can't be opened"** — Run `xattr -cr /Applications/ClifPad.app` to remove the quarantine flag. +**AI Your Way** — OpenRouter for 100+ cloud models. Ollama for fully local inference. Claude Code CLI integration. Ghost text completions. All optional, all opt-in. + +**5 Themes** — Midnight, Graphite, Dawn, Arctic, Dusk. Dark and light. Switch instantly. + +
+ +### Install ClifPad + +Download the latest build from [Releases](https://github.com/DLhugly/Clif-Code/releases): + +| Platform | Download | +|----------|----------| +| macOS (Apple Silicon) | [`.dmg`](https://github.com/DLhugly/Clif-Code/releases) | +| macOS (Intel) | [`.dmg`](https://github.com/DLhugly/Clif-Code/releases) | +| Windows | [`.exe`](https://github.com/DLhugly/Clif-Code/releases) | +| Linux | [`.deb`](https://github.com/DLhugly/Clif-Code/releases) | + +> **macOS users:** If you see "App can't be opened", run `xattr -cr /Applications/ClifPad.app` — this removes Apple's quarantine flag on unsigned apps. Notarization is on the roadmap. + +
--- -## ClifCode +
-AI coding assistant TUI that runs in any terminal. Supports OpenRouter, OpenAI, Anthropic, Ollama, and any OpenAI-compatible API. +## ClifCode — The Agent -### Features +> Like Claude Code, but open source. Runs anywhere, talks to any model. -- **Tool-calling agent loop** — read/write files, run commands, search, git ops -- **Streaming markdown** — line-buffered rendering with syntax highlighting -- **Parallel tools** — read-only calls execute concurrently -- **Session persistence** — auto-saves, resume previous sessions -- **Cost tracking** — per-turn and session token usage +ClifCode is an AI coding agent that lives in your terminal. Give it a task, and it reads your codebase, writes code, runs commands, and commits — all through a tool-calling agent loop. + +``` + ◆ Model anthropic/claude-sonnet-4 ◆ Mode auto-edit + ◆ Path ~/projects/my-app -### Install + Type a task to get started, or /help for commands + ───────────────────────────────────────────── + + ❯ add dark mode to the settings page +``` + +### What it does + +- **Agentic tool loop** — reads files, writes code, runs shell commands, searches your codebase, manages git — up to 7 tool calls per turn, automatically +- **Any model, any provider** — OpenRouter, OpenAI, Anthropic, Ollama, or any OpenAI-compatible API. Swap providers with one flag +- **Parallel tool execution** — read-only calls (file reads, searches) run concurrently on threads for speed +- **Streaming markdown** — responses render live in your terminal with syntax highlighting +- **3 autonomy modes** — `suggest` (confirm every write), `auto-edit` (apply with collapsed diffs), `full-auto` (hands-off) +- **Session persistence** — conversations auto-save. Resume any session later with `/resume` +- **Auto-commit** — optionally commits changes with descriptive messages. Undo with `/undo` +- **Cost tracking** — per-turn and session-wide token usage and estimated cost +- **Workspace intelligence** — auto-scans your project structure, reads README/config files for context +- **Non-interactive mode** — pipe in a prompt with `clifcode -p "fix the login bug"` for scripting + +### 9 Built-in Tools + +| Tool | What it does | +|------|-------------| +| `read_file` | Read files with optional offset for large files | +| `write_file` | Create new files | +| `edit_file` | Surgical find-and-replace edits with diff preview | +| `find_file` | Locate files by name across the workspace | +| `list_files` | Directory listing with structure | +| `search` | Regex search across your codebase | +| `run_command` | Execute shell commands | +| `change_directory` | Switch workspace context | +| `submit` | Signal task completion with auto-commit | + +### Install ClifCode ```bash npm i -g clifcode @@ -83,32 +161,69 @@ npm i -g clifcode Or build from source: ```bash -cd clif-code-tui -cargo install --path . +cd clif-code-tui && cargo install --path . ``` ### Usage ```bash -clifcode # auto-detect backend -clifcode --backend api --api-model gpt-4o # specific API model -clifcode --backend ollama # local Ollama server +clifcode # interactive — auto-detect backend +clifcode -p "explain this codebase" # non-interactive — single prompt +clifcode --backend api --api-model gpt-4o # specific model +clifcode --backend ollama # local Ollama server +clifcode --autonomy suggest # confirm every file write +clifcode --resume # pick up where you left off ``` +### Slash Commands + +``` +Session: /new /sessions /resume /cost /clear /quit +Workspace: /cd /add /drop /context +Settings: /mode /backend /config +Git: /status /undo +``` + +
+ --- -## Development +
+ +## Getting Started ```bash -# ClifPad +# Clone +git clone https://github.com/DLhugly/Clif-Code.git && cd Clif-Code + +# ClifPad — desktop IDE cd clif-pad-ide && npm install && npm run tauri dev -# ClifCode +# ClifCode — terminal agent cd clif-code-tui && cargo run --release ``` +### Requirements + +| | ClifPad | ClifCode | +|---|---------|----------| +| **Runtime** | Node 18+, Rust | Rust | +| **Install** | [Download](https://github.com/DLhugly/Clif-Code/releases) | `npm i -g clifcode` | +| **Size** | ~20MB | ~5MB | +| **AI needed?** | Optional | Yes (any provider) | + +
+ +--- + +
+ +## Contributing + [Conventional commits](https://www.conventionalcommits.org/) — `feat:` bumps minor, `fix:` bumps patch, `feat!:` bumps major. +PRs welcome. The codebase is small enough to understand in an afternoon. + ## License -[MIT](LICENSE) +[MIT](LICENSE) — do whatever you want. From 9db497c58ee42a15b24b2aeff792e5f6533d02ca Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Sun, 1 Mar 2026 18:09:13 +0800 Subject: [PATCH 5/8] docs: add gradient SVG logo matching terminal ASCII art colors --- README.md | 13 ++++--------- logo.svg | 11 +++++++++++ 2 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 logo.svg diff --git a/README.md b/README.md index 8519a55..10ec3a6 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@

- Clif-Code + ClifCode

-
-   _____ _ _  __ _____          _
-  / ____| (_)/ _/ ____|        | |
- | |    | |_| || |     ___   __| | ___
- | |    | | |  _| |    / _ \ / _` |/ _ \
- | |____| | | | | |___| (_) | (_| |  __/
-  \_____|_|_|_|  \_____\___/ \__,_|\___|
-
+

+ Clif-Code +

Two tools. One mission. Ship faster.

diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..09c36dd --- /dev/null +++ b/logo.svg @@ -0,0 +1,11 @@ + + + + _____ _ _ __ _____ _ + / ____| (_)/ _/ ____| | | + | | | |_| || | ___ __| | ___ + | | | | | _| | / _ \ / _` |/ _ \ + | |____| | | | | |___| (_) | (_| | __/ + \_____|_|_|_| \_____\___/ \__,_|\___| + + From 5a29f9b08c49bc0d41ddefe6ffe6790bd50bfab3 Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Sun, 1 Mar 2026 23:02:16 +0800 Subject: [PATCH 6/8] docs: rewrite README with TUI colors, emojis, comparison table, and tool showcase --- README.md | 326 ++++++++++++++++++++++++++++++++++-------------------- logo.svg | 30 +++-- 2 files changed, 226 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 10ec3a6..3f3be09 100644 --- a/README.md +++ b/README.md @@ -1,224 +1,308 @@

- ClifCode + + + + ClifCode +

- Clif-Code + Desktop IDE + Terminal Agent. Both native. Both open source.

-

Two tools. One mission. Ship faster.

-

- ClifPad — ~20MB desktop IDE that replaces Electron bloat
- ClifCode — AI agent that lives in your terminal + MIT License + Release + npm + Rust + Platform

- MIT License - Release - npm - Platform + 🌐 Website  ·  + 🖥️ ClifPad  ·  + ⚡ ClifCode  ·  + 🚀 Quick Start  ·  + 📦 Downloads

+
+

- Website · - ClifPad · - ClifCode · - Get Started · - Downloads + ClifPad Screenshot

+
+ --- -
+## 🧬 What is Clif? -## Why Clif? +Clif is a monorepo with **two products** that share one philosophy: coding tools should be **fast**, **private**, and **yours**. -Every other "AI editor" ships a 400MB Electron wrapper, locks you into a subscription, and phones home with your code. +``` +Clif-Code/ +├── clif-pad-ide/ 🖥️ Desktop IDE — Tauri 2 + SolidJS + Monaco +├── clif-code-tui/ ⚡ Terminal AI agent — pure Rust, any API +└── .github/ 🔄 CI/CD (auto-release, npm publish) +``` -Clif is different: + + + + + +
+ +### 🖥️ ClifPad +**~20MB** native desktop editor
+Monaco · Terminal · Git · AI · 5 Themes
+Tauri 2 (Rust) + SolidJS + Tailwind CSS 4

+⬇️ Download -- **Native.** Rust backend, ~20MB binary. Launches in under a second. -- **Private.** Zero telemetry. Zero cloud sync. Your code never leaves your machine. -- **Open.** MIT licensed. Read every line. Fork it. Ship it. No strings. -- **AI on your terms.** Bring your own key — OpenRouter, OpenAI, Anthropic, Ollama. Or turn it all off and use a pure offline editor. +
+ +### ⚡ ClifCode +**AI agent** in your terminal
+9 tools · sessions · auto-commit · streaming
+Pure Rust · OpenRouter · OpenAI · Ollama

+npm i -g clifcode + +

-``` -Clif-Code/ -├── clif-pad-ide/ Desktop IDE — Tauri 2 + SolidJS + Monaco -├── clif-code-tui/ Terminal AI agent — pure Rust, any API -└── .github/ CI/CD (auto-release, npm publish) -``` +> **Why not Cursor / VS Code / Zed?** +> +> | | Clif | Cursor | VS Code | Zed | +> |---|:---:|:---:|:---:|:---:| +> | **Binary size** | **~20MB** 🟢 | ~400MB 🔴 | ~350MB 🔴 | ~100MB 🟡 | +> | **UI runtime** | **7KB** (SolidJS) | Electron | Electron | GPU | +> | **RAM at idle** | **~80MB** | ~500MB+ | ~400MB+ | ~150MB | +> | **Telemetry** | **Zero** | Yes | Yes | Yes | +> | **Subscription** | **None** | $20/mo | Free* | Free* | +> | **Open source** | **MIT** | No | Partial | Yes |
--- -
+## 🖥️ ClifPad -## ClifPad — The IDE +> **VS Code features at 1/20th the size. No Electron. No telemetry. No subscription.** -> VS Code features. Fraction of the size. No Electron in sight. +A native desktop code editor built with Tauri 2 (Rust) and SolidJS. The same Monaco engine that powers VS Code — wrapped in a binary that launches instantly and barely touches your RAM. - - - - - -
+### ✨ Features -**Monaco Editor** — The same engine behind VS Code. 70+ languages, IntelliSense, multi-cursor, minimap, bracket matching, code folding. Full editing power in a native shell. +| | Feature | Details | +|---|---------|---------| +| 📝 | **Monaco Editor** | 70+ languages, IntelliSense, multi-cursor, minimap, bracket matching, code folding | +| 🖥️ | **Real Terminal** | Native PTY via Rust — your actual shell with 256-color, resize, 10K scrollback | +| 🔍 | **Dev Preview** | One-click dev server, auto-detects localhost, live iframe preview | +| 🌿 | **Git Integration** | Branch, stage, commit, per-file diff stats, visual commit graph — all in Rust | +| 🤖 | **AI (opt-in)** | OpenRouter (100+ models), Ollama (local), Claude Code CLI, ghost text completions | +| 🎨 | **5 Themes** | Midnight · Graphite · Dawn · Arctic · Dusk | -**Real Terminal** — Not a web simulation. Native PTY sessions via Rust with 256-color, auto-resize, and 10K scrollback. Your actual shell, running at full speed. +### 📦 Install -**Dev Preview** — One-click dev server launcher. Auto-detects localhost ports and renders a live iframe preview. Build and see changes without leaving the editor. +**Download** from [Releases](https://github.com/DLhugly/Clif-Code/releases) — available for macOS (Apple Silicon + Intel), Windows, and Linux. - +Or build from source: -**Git Built In** — Branch, stage, commit, per-file diff stats, visual commit graph. All powered by Rust — not shell commands piped through a webview. +```bash +git clone https://github.com/DLhugly/Clif-Code.git +cd Clif-Code/clif-pad-ide +npm install && npm run tauri dev +``` -**AI Your Way** — OpenRouter for 100+ cloud models. Ollama for fully local inference. Claude Code CLI integration. Ghost text completions. All optional, all opt-in. +> [!NOTE] +> **macOS "App can't be opened"** — Run `xattr -cr /Applications/ClifPad.app` to remove the quarantine flag. This is standard for unsigned open-source apps. Notarization is on the roadmap. -**5 Themes** — Midnight, Graphite, Dawn, Arctic, Dusk. Dark and light. Switch instantly. +
-
+--- -### Install ClifPad +## ⚡ ClifCode -Download the latest build from [Releases](https://github.com/DLhugly/Clif-Code/releases): +> **Open-source AI coding agent for your terminal. Like Claude Code — but you own it.** -| Platform | Download | -|----------|----------| -| macOS (Apple Silicon) | [`.dmg`](https://github.com/DLhugly/Clif-Code/releases) | -| macOS (Intel) | [`.dmg`](https://github.com/DLhugly/Clif-Code/releases) | -| Windows | [`.exe`](https://github.com/DLhugly/Clif-Code/releases) | -| Linux | [`.deb`](https://github.com/DLhugly/Clif-Code/releases) | +ClifCode is a tool-calling AI agent that reads your codebase, writes code, runs commands, searches files, and auto-commits — all from a beautiful TUI. Works with **any** OpenAI-compatible API. -> **macOS users:** If you see "App can't be opened", run `xattr -cr /Applications/ClifPad.app` — this removes Apple's quarantine flag on unsigned apps. Notarization is on the roadmap. +### 🚀 Get started -
+```bash +npm i -g clifcode +``` ---- +That's it. Run `clifcode` in any project directory. -
+
+Other install methods + +```bash +# Build from source +cd clif-code-tui && cargo install --path . -## ClifCode — The Agent +# Or just run it +cd clif-code-tui && cargo run --release +``` -> Like Claude Code, but open source. Runs anywhere, talks to any model. +
-ClifCode is an AI coding agent that lives in your terminal. Give it a task, and it reads your codebase, writes code, runs commands, and commits — all through a tool-calling agent loop. +### 🎬 How it looks ``` + _____ _ _ __ _____ _ + / ____| (_)/ _/ ____| | | + | | | |_| || | ___ __| | ___ + | | | | | _| | / _ \ / _` |/ _ \ + | |____| | | | | |___| (_) | (_| | __/ + \_____|_|_|_| \_____\___/ \__,_|\___| + + AI coding assistant — works anywhere, ships fast + ◆ Model anthropic/claude-sonnet-4 ◆ Mode auto-edit ◆ Path ~/projects/my-app Type a task to get started, or /help for commands ───────────────────────────────────────────── - ❯ add dark mode to the settings page -``` + ❯ refactor the auth module to use JWT tokens -### What it does + [1/7] ••• thinking + ▶ read src/auth/mod.rs + ▶ read src/auth/session.rs + ◇ find config.toml + ✎ edit src/auth/mod.rs +42 -18 + ✎ edit src/auth/session.rs +15 -8 + ▸ run cargo test + ✓ All 23 tests passed -- **Agentic tool loop** — reads files, writes code, runs shell commands, searches your codebase, manages git — up to 7 tool calls per turn, automatically -- **Any model, any provider** — OpenRouter, OpenAI, Anthropic, Ollama, or any OpenAI-compatible API. Swap providers with one flag -- **Parallel tool execution** — read-only calls (file reads, searches) run concurrently on threads for speed -- **Streaming markdown** — responses render live in your terminal with syntax highlighting -- **3 autonomy modes** — `suggest` (confirm every write), `auto-edit` (apply with collapsed diffs), `full-auto` (hands-off) -- **Session persistence** — conversations auto-save. Resume any session later with `/resume` -- **Auto-commit** — optionally commits changes with descriptive messages. Undo with `/undo` -- **Cost tracking** — per-turn and session-wide token usage and estimated cost -- **Workspace intelligence** — auto-scans your project structure, reads README/config files for context -- **Non-interactive mode** — pipe in a prompt with `clifcode -p "fix the login bug"` for scripting + ✦ ClifCode Refactored auth module to use JWT tokens. + Replaced session-based auth with stateless JWT + verification. Added token expiry and refresh logic. -### 9 Built-in Tools + ∙ 2.1k tokens ∙ ~$0.0312 +``` -| Tool | What it does | -|------|-------------| -| `read_file` | Read files with optional offset for large files | -| `write_file` | Create new files | -| `edit_file` | Surgical find-and-replace edits with diff preview | -| `find_file` | Locate files by name across the workspace | -| `list_files` | Directory listing with structure | -| `search` | Regex search across your codebase | -| `run_command` | Execute shell commands | -| `change_directory` | Switch workspace context | -| `submit` | Signal task completion with auto-commit | +### 🛠️ Features -### Install ClifCode +| | Feature | Details | +|---|---------|---------| +| 🔄 | **Agentic loop** | Up to 7 tool calls per turn — reads, writes, runs, searches, commits automatically | +| 🌐 | **Any provider** | OpenRouter, OpenAI, Anthropic, Ollama, or any OpenAI-compatible endpoint | +| ⚡ | **Parallel tools** | Read-only calls (file reads, searches) execute concurrently on threads | +| 📡 | **Streaming** | Responses render live with markdown formatting, code blocks, and syntax hints | +| 🎛️ | **3 autonomy modes** | `suggest` — confirm writes · `auto-edit` — apply with diffs · `full-auto` — hands-off | +| 💾 | **Sessions** | Auto-saves every conversation. Resume any session with `/resume` | +| 🔀 | **Auto-commit** | Commits changes with descriptive messages. One-command `/undo` | +| 💰 | **Cost tracking** | Per-turn and session-wide token usage with estimated cost | +| 🧠 | **Workspace intel** | Auto-scans project structure, reads README/Cargo.toml/package.json for context | +| 🔧 | **Non-interactive** | `clifcode -p "fix the bug"` for scripts and CI | -```bash -npm i -g clifcode -``` +### 🔧 9 Built-in Tools -Or build from source: - -```bash -cd clif-code-tui && cargo install --path . +``` + ▶ read_file Read files (with offset for large files) + ✎ write_file Create new files + ✎ edit_file Surgical find-and-replace with diff preview + ◇ find_file Locate files by name across the workspace + ☰ list_files Directory listing with structure + ⌕ search Regex search across your codebase + ▸ run_command Execute shell commands + → change_directory Switch workspace context + ✓ submit Signal task completion + auto-commit ``` -### Usage +### 💻 Usage ```bash -clifcode # interactive — auto-detect backend -clifcode -p "explain this codebase" # non-interactive — single prompt +clifcode # interactive, auto-detect backend +clifcode -p "explain this codebase" # non-interactive single prompt clifcode --backend api --api-model gpt-4o # specific model -clifcode --backend ollama # local Ollama server -clifcode --autonomy suggest # confirm every file write -clifcode --resume # pick up where you left off +clifcode --backend ollama # local Ollama +clifcode --autonomy suggest # confirm every write +clifcode --resume # resume last session ``` -### Slash Commands +### ⌨️ Commands ``` -Session: /new /sessions /resume /cost /clear /quit -Workspace: /cd /add /drop /context -Settings: /mode /backend /config -Git: /status /undo + ◆ Session /new /sessions /resume /cost /clear /quit + ◆ Workspace /cd /add /drop /context + ◆ Settings /mode /backend /config + ◆ Git /status /undo ``` -
+### 🔌 Supported Providers ---- +| Provider | Config | +|----------|--------| +| **OpenRouter** (default) | `CLIFCODE_API_KEY` — access to 100+ models | +| **OpenAI** | `--api-url https://api.openai.com/v1` | +| **Anthropic** | Via OpenRouter or compatible proxy | +| **Ollama** | `--backend ollama` — runs fully local, no API key needed | +| **Any OpenAI-compatible** | `--api-url ` |
-## Getting Started +--- + +## 🚀 Quick Start ```bash -# Clone +# Clone the monorepo git clone https://github.com/DLhugly/Clif-Code.git && cd Clif-Code +``` -# ClifPad — desktop IDE +**ClifPad** (desktop IDE): +```bash cd clif-pad-ide && npm install && npm run tauri dev +``` -# ClifCode — terminal agent +**ClifCode** (terminal agent): +```bash cd clif-code-tui && cargo run --release ``` +**Or install ClifCode globally:** +```bash +npm i -g clifcode && clifcode +``` + ### Requirements | | ClifPad | ClifCode | |---|---------|----------| -| **Runtime** | Node 18+, Rust | Rust | -| **Install** | [Download](https://github.com/DLhugly/Clif-Code/releases) | `npm i -g clifcode` | -| **Size** | ~20MB | ~5MB | -| **AI needed?** | Optional | Yes (any provider) | +| **Language** | Rust + TypeScript | Rust | +| **Runtime** | Node 18+, Rust stable | Rust stable | +| **Install** | [Download binary](https://github.com/DLhugly/Clif-Code/releases) | `npm i -g clifcode` | +| **Binary size** | ~20MB | ~5MB | +| **AI required?** | No (opt-in) | Yes (any provider) |
--- -
+## 🤝 Contributing + +We use [conventional commits](https://www.conventionalcommits.org/): + +- `feat:` — new feature (bumps minor) +- `fix:` — bug fix (bumps patch) +- `feat!:` — breaking change (bumps major) -## Contributing +The codebase is intentionally small. ClifPad's frontend is ~2K lines of SolidJS. ClifCode's agent is ~1K lines of Rust. You can read and understand either project in an afternoon. -[Conventional commits](https://www.conventionalcommits.org/) — `feat:` bumps minor, `fix:` bumps patch, `feat!:` bumps major. +**PRs welcome.** -PRs welcome. The codebase is small enough to understand in an afternoon. +## 📜 License -## License +[MIT](LICENSE) — use it however you want. -[MIT](LICENSE) — do whatever you want. +
+ +

+ Built with 🦀 Rust and ❤️ by DLhugly +

diff --git a/logo.svg b/logo.svg index 09c36dd..5e98102 100644 --- a/logo.svg +++ b/logo.svg @@ -1,11 +1,23 @@ - - - - _____ _ _ __ _____ _ - / ____| (_)/ _/ ____| | | - | | | |_| || | ___ __| | ___ - | | | | | _| | / _ \ / _` |/ _ \ - | |____| | | | | |___| (_) | (_| | __/ - \_____|_|_|_| \_____\___/ \__,_|\___| + + + + + + + + + + + + + + + _____ _ _ __ _____ _ + / ____| (_)/ _/ ____| | | + | | | |_| || | ___ __| | ___ + | | | | | _| | / _ \ / _` |/ _ \ + | |____| | | | | |___| (_) | (_| | __/ + \_____|_|_|_| \_____\___/ \__,_|\___| + AI-native code editor & terminal agent From d2df08a59028262013cb4852f9b11578ed1853cb Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Sun, 1 Mar 2026 23:08:42 +0800 Subject: [PATCH 7/8] docs: merge original ClifPad detail back into README Restores download badges with platform logos, architecture diagram, size comparison table, keyboard shortcuts, project structure, FAQ, and macOS quarantine instructions from original README while keeping the new ClifCode agent section and SVG logo. --- README.md | 254 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 145 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 3f3be09..fea8d51 100644 --- a/README.md +++ b/README.md @@ -7,38 +7,38 @@

- Desktop IDE + Terminal Agent. Both native. Both open source. + ~20MB desktop IDE. Terminal AI agent. Both native. Both open source.

MIT License Release npm - Rust + ~20MB + 7KB runtime + Tauri 2 Platform

🌐 Website  ·  - 🖥️ ClifPad  ·  - ⚡ ClifCode  ·  - 🚀 Quick Start  ·  - 📦 Downloads + 🖥️ ClifPad  ·  + ⚡ ClifCode  ·  + 🛠️ Development  ·  + 📦 Releases

-
- -

- ClifPad Screenshot -

+--- -
+Cursor is 400MB. VS Code is 350MB. Zed doesn't do AI. ---- +**Clif is ~20MB.** A native Rust binary with a 7KB SolidJS frontend. VS Code-quality editing via Monaco. Real terminal via PTY. Git built into the backend. AI when you want it, silence when you don't. -## 🧬 What is Clif? +No Electron. No telemetry. No subscription. Open source. -Clif is a monorepo with **two products** that share one philosophy: coding tools should be **fast**, **private**, and **yours**. +

+ ClifPad Screenshot +

``` Clif-Code/ @@ -47,89 +47,100 @@ Clif-Code/ └── .github/ 🔄 CI/CD (auto-release, npm publish) ``` - - - - - -
- -### 🖥️ ClifPad -**~20MB** native desktop editor
-Monaco · Terminal · Git · AI · 5 Themes
-Tauri 2 (Rust) + SolidJS + Tailwind CSS 4

-⬇️ Download +--- -
+## 🖥️ ClifPad — Download -### ⚡ ClifCode -**AI agent** in your terminal
-9 tools · sessions · auto-commit · streaming
-Pure Rust · OpenRouter · OpenAI · Ollama

-npm i -g clifcode +

+ Apple Silicon +   + Intel +

-
+

+ Windows +   + Linux deb +   + Linux AppImage +

-
+> [All releases & checksums](https://github.com/DLhugly/Clif-Code/releases) -> **Why not Cursor / VS Code / Zed?** -> -> | | Clif | Cursor | VS Code | Zed | -> |---|:---:|:---:|:---:|:---:| -> | **Binary size** | **~20MB** 🟢 | ~400MB 🔴 | ~350MB 🔴 | ~100MB 🟡 | -> | **UI runtime** | **7KB** (SolidJS) | Electron | Electron | GPU | -> | **RAM at idle** | **~80MB** | ~500MB+ | ~400MB+ | ~150MB | -> | **Telemetry** | **Zero** | Yes | Yes | Yes | -> | **Subscription** | **None** | $20/mo | Free* | Free* | -> | **Open source** | **MIT** | No | Partial | Yes | +### macOS — "App can't be opened" -
+Clif is open source but not yet notarized with Apple ($99/year). macOS blocks unsigned apps by default. This is normal for open source software — run one command to fix it: ---- +```bash +xattr -cr /Applications/ClifPad.app +``` -## 🖥️ ClifPad +Then open ClifPad normally. This removes the quarantine flag that macOS sets on downloads. [Why does this happen?](#-faq) -> **VS Code features at 1/20th the size. No Electron. No telemetry. No subscription.** +**From source:** -A native desktop code editor built with Tauri 2 (Rust) and SolidJS. The same Monaco engine that powers VS Code — wrapped in a binary that launches instantly and barely touches your RAM. +```bash +git clone https://github.com/DLhugly/Clif-Code.git && cd Clif-Code +cd clif-pad-ide && npm install && npm run tauri dev +``` ### ✨ Features -| | Feature | Details | -|---|---------|---------| -| 📝 | **Monaco Editor** | 70+ languages, IntelliSense, multi-cursor, minimap, bracket matching, code folding | -| 🖥️ | **Real Terminal** | Native PTY via Rust — your actual shell with 256-color, resize, 10K scrollback | -| 🔍 | **Dev Preview** | One-click dev server, auto-detects localhost, live iframe preview | -| 🌿 | **Git Integration** | Branch, stage, commit, per-file diff stats, visual commit graph — all in Rust | -| 🤖 | **AI (opt-in)** | OpenRouter (100+ models), Ollama (local), Claude Code CLI, ghost text completions | -| 🎨 | **5 Themes** | Midnight · Graphite · Dawn · Arctic · Dusk | +**📝 Monaco Editor** — 70+ languages, IntelliSense, multi-cursor, minimap, code folding. The same engine as VS Code. -### 📦 Install +**🖥️ Real Terminal** — Native PTY via Rust. Your actual shell with 256-color, resize, 10K scrollback. Not a simulation. -**Download** from [Releases](https://github.com/DLhugly/Clif-Code/releases) — available for macOS (Apple Silicon + Intel), Windows, and Linux. +**🔍 Dev Preview** — One-click `npm run dev`, auto-detects `localhost` URLs, live iframe preview. Run and see your app without switching windows. -Or build from source: +**🌿 Git** — Branch, status, stage, commit, per-file `+/-` diff stats, visual commit graph. All Rust, no shelling out. -```bash -git clone https://github.com/DLhugly/Clif-Code.git -cd Clif-Code/clif-pad-ide -npm install && npm run tauri dev -``` +**🤖 AI** — OpenRouter (Claude, GPT-4, Gemini, 100+ models), Ollama (fully local), Claude Code CLI. Ghost text completions. All opt-in. Works fine offline with zero keys. -> [!NOTE] -> **macOS "App can't be opened"** — Run `xattr -cr /Applications/ClifPad.app` to remove the quarantine flag. This is standard for unsigned open-source apps. Notarization is on the roadmap. +**🎨 5 Themes** — Midnight, Graphite, Dawn, Arctic, Dusk. Editor, terminal, and UI stay in sync. -
+**⌨️ Keys** — `Ctrl+`` ` terminal, `Ctrl+B` sidebar, `Ctrl+S` save, `Ctrl+Shift+P` palette. ---- +### 📊 The Size Flex -## ⚡ ClifCode +| | Binary | Runtime | RAM idle | +|---|--------|---------|----------| +| **ClifPad** | **~20MB** 🟢 | **7KB** 🟢 | **~80MB** 🟢 | +| Cursor | ~400MB 🔴 | ~50MB 🔴 | ~500MB+ 🔴 | +| VS Code | ~350MB 🔴 | ~40MB 🔴 | ~400MB+ 🔴 | +| Zed | ~100MB 🟡 | native | ~200MB 🟡 | -> **Open-source AI coding agent for your terminal. Like Claude Code — but you own it.** +Tauri 2 compiles to a single native binary. SolidJS has no virtual DOM overhead. Rust handles all heavy lifting — file I/O, git, PTY, AI streaming — with zero garbage collection. + +### 🏗️ Architecture -ClifCode is a tool-calling AI agent that reads your codebase, writes code, runs commands, searches files, and auto-commits — all from a beautiful TUI. Works with **any** OpenAI-compatible API. +``` +┌─────────────────────────────────────────┐ +│ Tauri 2 (Rust) │ +│ File I/O · Git · PTY · AI · Search │ +│ │ │ +│ IPC (invoke/events) │ +│ │ │ +│ SolidJS + TypeScript │ +│ Monaco Editor · xterm.js │ +│ Tailwind CSS 4 │ +└─────────────────────────────────────────┘ +``` -### 🚀 Get started +| Layer | Tech | Size | +|-------|------|------| +| Backend | Tauri 2 + Rust | ~20MB compiled | +| UI | SolidJS | 7KB runtime | +| Editor | Monaco | tree-shaken | +| Terminal | xterm.js + portable-pty | real PTY | +| Styles | Tailwind CSS 4 | zero runtime | +| Build | Vite 6 | <5s HMR | +| CI/CD | Semantic Release | auto-versioned | + +--- + +## ⚡ ClifCode — Install + +> **Open-source AI coding agent for your terminal. Like Claude Code — but you own it.** ```bash npm i -g clifcode @@ -142,9 +153,10 @@ That's it. Run `clifcode` in any project directory. ```bash # Build from source +git clone https://github.com/DLhugly/Clif-Code.git && cd Clif-Code cd clif-code-tui && cargo install --path . -# Or just run it +# Or just run it directly cd clif-code-tui && cargo run --release ``` @@ -152,6 +164,8 @@ cd clif-code-tui && cargo run --release ### 🎬 How it looks +ClifCode is a tool-calling AI agent that reads your codebase, writes code, runs commands, searches files, and auto-commits — all from a beautiful TUI. + ``` _____ _ _ __ _____ _ / ____| (_)/ _/ ____| | | @@ -242,60 +256,78 @@ clifcode --resume # resume last session | **OpenRouter** (default) | `CLIFCODE_API_KEY` — access to 100+ models | | **OpenAI** | `--api-url https://api.openai.com/v1` | | **Anthropic** | Via OpenRouter or compatible proxy | -| **Ollama** | `--backend ollama` — runs fully local, no API key needed | +| **Ollama** | `--backend ollama` — fully local, no API key needed | | **Any OpenAI-compatible** | `--api-url ` | -
- --- -## 🚀 Quick Start +## 🛠️ Development ```bash -# Clone the monorepo -git clone https://github.com/DLhugly/Clif-Code.git && cd Clif-Code +# ClifPad — desktop IDE +cd clif-pad-ide +npm install && npm run tauri dev # dev mode + hot reload +npm run tauri build # production binary + +# ClifCode — terminal agent +cd clif-code-tui +cargo run --release # run directly +cargo install --path . # install to PATH ``` -**ClifPad** (desktop IDE): -```bash -cd clif-pad-ide && npm install && npm run tauri dev -``` +### Project Structure -**ClifCode** (terminal agent): -```bash -cd clif-code-tui && cargo run --release ``` - -**Or install ClifCode globally:** -```bash -npm i -g clifcode && clifcode +clif-pad-ide/ +├── src/ # SolidJS frontend +│ ├── components/ # editor, terminal, layout, explorer +│ ├── stores/ # reactive state (signals + stores) +│ ├── lib/ # IPC wrappers, keybindings, themes +│ └── types/ # TypeScript interfaces +├── src-tauri/src/ # Rust backend +│ ├── commands/ # fs, git, pty, ai, search, settings +│ └── services/ # file watcher, ai providers +└── www/ # Landing page (clifcode.io) + +clif-code-tui/ +├── src/ +│ ├── main.rs # CLI, TUI loop, agent orchestration +│ ├── backend.rs # API backend (OpenRouter, OpenAI, Ollama) +│ ├── tools.rs # Tool definitions and execution +│ ├── ui.rs # Terminal UI rendering +│ ├── session.rs # Session persistence +│ ├── config.rs # Config (API keys, provider setup) +│ ├── git.rs # Git integration +│ └── repomap.rs # Workspace structure analysis +├── npm/ # npm distribution packages +│ ├── clifcode/ # Main wrapper (npm i -g clifcode) +│ └── @clifcode/cli-*/ # 6 platform-specific binaries +└── scripts/ + └── bump-version.js # Syncs versions across Cargo.toml + npm ``` -### Requirements +[Conventional commits](https://www.conventionalcommits.org/) — `feat:` bumps minor, `fix:` bumps patch, `feat!:` bumps major. Semantic release handles the rest. -| | ClifPad | ClifCode | -|---|---------|----------| -| **Language** | Rust + TypeScript | Rust | -| **Runtime** | Node 18+, Rust stable | Rust stable | -| **Install** | [Download binary](https://github.com/DLhugly/Clif-Code/releases) | `npm i -g clifcode` | -| **Binary size** | ~20MB | ~5MB | -| **AI required?** | No (opt-in) | Yes (any provider) | +--- -
+## ❓ FAQ ---- +**Why does macOS say "App can't be opened"?** +macOS Gatekeeper blocks apps that aren't signed with a $99/year Apple Developer certificate. ClifPad is open source and safe — run `xattr -cr /Applications/ClifPad.app` in Terminal to remove the quarantine flag, then open normally. -## 🤝 Contributing +**Is Clif safe?** +100% open source. Read every line: [github.com/DLhugly/Clif-Code](https://github.com/DLhugly/Clif-Code). No telemetry, no network calls unless you enable AI. The `xattr` command just removes Apple's download flag — it doesn't disable any security. -We use [conventional commits](https://www.conventionalcommits.org/): +**Why not just pay for code signing?** +We will. For now, the $99/year Apple Developer fee goes toward more important things. Proper signing + notarization is on the roadmap. -- `feat:` — new feature (bumps minor) -- `fix:` — bug fix (bumps patch) -- `feat!:` — breaking change (bumps major) +**Does it work offline?** +ClifPad: Yes — AI features are opt-in. Without API keys, it's a fully offline editor with terminal and git. ClifCode: Needs an API provider (but Ollama runs fully local with no internet). -The codebase is intentionally small. ClifPad's frontend is ~2K lines of SolidJS. ClifCode's agent is ~1K lines of Rust. You can read and understand either project in an afternoon. +**What models does ClifCode support?** +Any OpenAI-compatible API. Default is `anthropic/claude-sonnet-4` via OpenRouter. Also works with GPT-4o, Gemini, Llama, Qwen, Mistral, DeepSeek — anything on OpenRouter or Ollama. -**PRs welcome.** +--- ## 📜 License @@ -303,6 +335,10 @@ The codebase is intentionally small. ClifPad's frontend is ~2K lines of SolidJS.
+

+ 20MB. Native. Private. Fast. +

+

Built with 🦀 Rust and ❤️ by DLhugly

From c2c40154dee12520b0ddc367d925e6425fc14779 Mon Sep 17 00:00:00 2001 From: James Lawrence Date: Sun, 1 Mar 2026 23:12:57 +0800 Subject: [PATCH 8/8] fix: update download URLs to ClifPad_ prefix and fix remaining clifpad path refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bump-version.js: Clif_ → ClifPad_ in download URL patterns, adds monorepo root README.md patching - www/index.html: ClifPad_ download URLs, ClifPad.app in xattr cmd - release.yml: remaining clifpad → clif-pad-ide working dirs - README.md: ClifPad_1.3.0_ versioned asset URLs --- .github/workflows/release.yml | 28 ++++++++++++++-------------- README.md | 10 +++++----- clif-pad-ide/scripts/bump-version.js | 22 ++++++++++++++++------ clif-pad-ide/www/index.html | 12 ++++++------ 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7882621..a962873 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,9 +23,9 @@ jobs: with: node-version: 22 cache-dependency-path: clif-pad-ide/package-lock.json - - run: cd clifpad && npm ci + - run: cd clif-pad-ide && npm ci - name: Semantic Release - working-directory: clifpad + working-directory: clif-pad-ide id: semantic uses: cycjimmy/semantic-release-action@v4 env: @@ -67,7 +67,7 @@ jobs: echo "Syncing version: $VERSION" git pull origin main || true sed -i '' "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" clif-pad-ide/src-tauri/tauri.conf.json - cd clifpad && node scripts/bump-version.js "$VERSION" + cd clif-pad-ide && node scripts/bump-version.js "$VERSION" echo "Version synchronized" - uses: actions/setup-node@v4 @@ -89,13 +89,13 @@ jobs: - name: Install Dependencies run: | - cd clifpad && npm ci + cd clif-pad-ide && npm ci echo "Node: $(node --version)" echo "Rust: $(rustc --version)" - name: Build Frontend run: | - cd clifpad && npm run build + cd clif-pad-ide && npm run build echo "Frontend built: $(du -sh dist/)" - name: Build Tauri App @@ -108,7 +108,7 @@ jobs: releaseBody: "See the assets to download and install ClifPad." releaseDraft: false prerelease: false - projectPath: clifpad + projectPath: clif-pad-ide args: --target ${{ matrix.target }} - name: Generate Checksums @@ -149,7 +149,7 @@ jobs: $VERSION = "${{ needs.semantic-release.outputs.new-release-version }}" echo "Syncing version: $VERSION" git pull origin main - cd clifpad && node scripts/bump-version.js "$VERSION" + cd clif-pad-ide && node scripts/bump-version.js "$VERSION" echo "Version synchronized" - uses: actions/setup-node@v4 @@ -168,10 +168,10 @@ jobs: cache-on-failure: true - name: Install Dependencies - run: cd clifpad && npm ci + run: cd clif-pad-ide && npm ci - name: Build Frontend - run: cd clifpad && npm run build + run: cd clif-pad-ide && npm run build - name: Build Tauri App uses: tauri-apps/tauri-action@v0 @@ -183,7 +183,7 @@ jobs: releaseBody: "See the assets to download and install ClifPad." releaseDraft: false prerelease: false - projectPath: clifpad + projectPath: clif-pad-ide - name: Generate Checksums shell: bash @@ -236,7 +236,7 @@ jobs: VERSION="${{ needs.semantic-release.outputs.new-release-version }}" echo "Syncing version: $VERSION" git pull origin main || true - cd clifpad && node scripts/bump-version.js "$VERSION" + cd clif-pad-ide && node scripts/bump-version.js "$VERSION" echo "Version synchronized" - uses: actions/setup-node@v4 @@ -255,10 +255,10 @@ jobs: cache-on-failure: true - name: Install Dependencies - run: cd clifpad && npm ci + run: cd clif-pad-ide && npm ci - name: Build Frontend - run: cd clifpad && npm run build + run: cd clif-pad-ide && npm run build - name: Build Tauri App uses: tauri-apps/tauri-action@v0 @@ -270,7 +270,7 @@ jobs: releaseBody: "See the assets to download and install ClifPad." releaseDraft: false prerelease: false - projectPath: clifpad + projectPath: clif-pad-ide - name: Generate Checksums run: | diff --git a/README.md b/README.md index fea8d51..d1519e1 100644 --- a/README.md +++ b/README.md @@ -52,17 +52,17 @@ Clif-Code/ ## 🖥️ ClifPad — Download

- Apple Silicon + Apple Silicon   - Intel + Intel

- Windows + Windows   - Linux deb + Linux deb   - Linux AppImage + Linux AppImage

> [All releases & checksums](https://github.com/DLhugly/Clif-Code/releases) diff --git a/clif-pad-ide/scripts/bump-version.js b/clif-pad-ide/scripts/bump-version.js index e8c2322..b25b424 100644 --- a/clif-pad-ide/scripts/bump-version.js +++ b/clif-pad-ide/scripts/bump-version.js @@ -60,16 +60,26 @@ readme = readme.replace( `## Download v${version}` ); -// Update all download URLs: /download/vOLD/Clif_OLD_ -> /download/vNEW/Clif_NEW_ +// Update all download URLs: /download/vOLD/ClifPad_OLD_ -> /download/vNEW/ClifPad_NEW_ readme = readme.replace( - /\/download\/v[\d.]+\/Clif_[\d.]+_/g, - `/download/v${version}/Clif_${version}_` + /\/download\/v[\d.]+\/ClifPad_[\d.]+_/g, + `/download/v${version}/ClifPad_${version}_` ); writeFileSync(readmePath, readme); console.log(` Updated README.md -> v${version}`); -// 5. www/index.html — version badge and download links +// 5. Monorepo root README.md — download links +const rootReadmePath = resolve(root, "..", "README.md"); +let rootReadme = readFileSync(rootReadmePath, "utf-8"); +rootReadme = rootReadme.replace( + /\/download\/v[\d.]+\/ClifPad_[\d.]+_/g, + `/download/v${version}/ClifPad_${version}_` +); +writeFileSync(rootReadmePath, rootReadme); +console.log(` Updated root README.md -> v${version}`); + +// 6. www/index.html — version badge and download links const wwwPath = resolve(root, "www", "index.html"); let www = readFileSync(wwwPath, "utf-8"); @@ -78,8 +88,8 @@ www = www.replace(/>v[\d.]+<\/div>/, `>v${version}
`); // Update download URLs www = www.replace( - /\/download\/v[\d.]+\/Clif_[\d.]+_/g, - `/download/v${version}/Clif_${version}_` + /\/download\/v[\d.]+\/ClifPad_[\d.]+_/g, + `/download/v${version}/ClifPad_${version}_` ); writeFileSync(wwwPath, www); diff --git a/clif-pad-ide/www/index.html b/clif-pad-ide/www/index.html index aabb448..449deb9 100644 --- a/clif-pad-ide/www/index.html +++ b/clif-pad-ide/www/index.html @@ -262,19 +262,19 @@

Code editor.
Not a browser.

~20MB native binary with AI built in. Monaco editing, real terminal, git integration. No Electron. No telemetry. No subscription.

- + Apple Silicon - + Intel Mac - + Windows - + Linux .deb @@ -411,9 +411,9 @@

Sessions

macOS — "App can't be opened"?

Clif is open source but not yet notarized with Apple. macOS blocks unsigned apps by default.
This is normal for open source software. One command fixes it:

- $ xattr -cr /Applications/Clif.app + $ xattr -cr /Applications/ClifPad.app
-

This removes the quarantine flag macOS sets on downloads. Then open Clif normally.

+

This removes the quarantine flag macOS sets on downloads. Then open ClifPad normally.