From c1896a7ff0ec3fc84f4a1013af04927a747b24dc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 6 Jun 2026 14:19:31 +0100 Subject: [PATCH 1/7] refac --- dev.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.sh b/dev.sh index 9b8ae374..b89b5036 100755 --- a/dev.sh +++ b/dev.sh @@ -1,3 +1,3 @@ #!/bin/bash export CPTR_DATA_DIR="${CPTR_DATA_DIR:-$(cd "$(dirname "$0")" && pwd)/.cptr}" -uv run cptr run --reload --port 9741 +uv run cptr run --reload --host 0.0.0.0 --port 9741 From 290faeede8da62861149d726c54f06a2ac5e7c86 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 6 Jun 2026 14:48:22 +0100 Subject: [PATCH 2/7] refac --- cptr/frontend/src/lib/apis/git.ts | 32 ++++++- .../src/lib/components/FileBrowser.svelte | 19 ++-- .../src/lib/components/FileEditor.svelte | 28 +++--- .../frontend/src/lib/components/GitBar.svelte | 33 ++----- .../src/lib/components/GitView.svelte | 45 ++++----- .../src/lib/stores/gitStatus.svelte.ts | 95 +++++++++++++++++++ cptr/frontend/src/routes/+layout.svelte | 27 ++---- cptr/routers/events.py | 24 ++++- 8 files changed, 209 insertions(+), 94 deletions(-) create mode 100644 cptr/frontend/src/lib/stores/gitStatus.svelte.ts diff --git a/cptr/frontend/src/lib/apis/git.ts b/cptr/frontend/src/lib/apis/git.ts index abec288c..b40a2b38 100644 --- a/cptr/frontend/src/lib/apis/git.ts +++ b/cptr/frontend/src/lib/apis/git.ts @@ -3,8 +3,36 @@ */ import { fetchJSON, jsonBody } from '$lib/apis'; -export const getGitStatus = (root: string) => - fetchJSON(`/api/git/status?root=${encodeURIComponent(root)}`); +// Deduplicate concurrent getGitStatus calls. +// Multiple components (layout, GitBar, FileEditor × N) all fetch on mount; +// this ensures they share a single in-flight request and a brief result cache. +const _statusCache = new Map; ts: number }>(); +const STATUS_CACHE_MS = 2000; + +export const getGitStatus = (root: string): Promise => { + const cached = _statusCache.get(root); + if (cached && Date.now() - cached.ts < STATUS_CACHE_MS) { + return cached.promise; + } + const promise = fetchJSON(`/api/git/status?root=${encodeURIComponent(root)}`); + _statusCache.set(root, { promise, ts: Date.now() }); + // Clean up after cache window + promise.finally(() => { + setTimeout(() => { + const entry = _statusCache.get(root); + if (entry && entry.promise === promise) { + _statusCache.delete(root); + } + }, STATUS_CACHE_MS); + }); + return promise; +}; + +/** Force a fresh git status fetch, bypassing the cache. */ +export const getGitStatusFresh = (root: string): Promise => { + _statusCache.delete(root); + return getGitStatus(root); +}; export const getGitLog = (root: string, limit = 30) => fetchJSON(`/api/git/log?root=${encodeURIComponent(root)}&limit=${limit}`); diff --git a/cptr/frontend/src/lib/components/FileBrowser.svelte b/cptr/frontend/src/lib/components/FileBrowser.svelte index 74f4d471..120a4a93 100644 --- a/cptr/frontend/src/lib/components/FileBrowser.svelte +++ b/cptr/frontend/src/lib/components/FileBrowser.svelte @@ -35,6 +35,7 @@ let entries = $state([]); let loading = $state(false); + let initialLoad = $state(true); let fetchTimer: ReturnType | null = null; let fetching = false; let error = $state(null); @@ -109,6 +110,7 @@ // Navigated to a new directory: reset tree state expandedDirs = new Set(); dirContents = new Map(); + initialLoad = true; } else { // Component (re)mount: restore tree state from cache const cachedExpanded = _treeExpandedCache.get(cwd); @@ -130,11 +132,12 @@ } }); - // Auto-refresh on filesystem changes (debounced) + // Auto-refresh on filesystem changes (debounced, silent) $effect(() => { const _tick = systemEvents.fsTick; // subscribe if (_tick > 0 && cwd && !fetching && systemEvents.isRelevantFsChange(cwd)) { - debouncedFetch(cwd); + // Use a longer debounce for auto-refresh to batch rapid fs events + debouncedFetch(cwd, 800); // Also refresh expanded directories for (const dir of expandedDirs) { fetchSubdir(dir); @@ -153,12 +156,12 @@ } } - function debouncedFetch(path: string) { + function debouncedFetch(path: string, delay = 300) { if (fetchTimer) clearTimeout(fetchTimer); fetchTimer = setTimeout(() => { fetchTimer = null; fetchDirectoryImmediate(path); - }, 300); + }, delay); } async function fetchDirectoryImmediate(path: string) { @@ -166,7 +169,10 @@ fetchTimer = null; if (fetching) return; // skip overlapping fetches fetching = true; - loading = true; + // Only show the loading spinner on initial load (no entries yet). + // Background refreshes silently update entries in-place. + const isInitial = entries.length === 0; + if (isInitial) loading = true; error = null; try { const data = await listDir(path); @@ -183,6 +189,7 @@ } } finally { loading = false; + initialLoad = false; fetching = false; } } @@ -752,7 +759,7 @@ {/if} - {#if loading} + {#if loading && initialLoad}
f.path === relPath); - } - } - } catch {} + const status = gitStatusStore.status; + const ws = get(activeWorkspace); + if (ws && status?.is_repo && status.files) { + const relPath = path.startsWith(ws.path) + ? path.slice(ws.path.replace(/\/$/, '').length + 1) + : path; + hasGitChanges = status.files.some((f) => f.path === relPath); + } } } diff --git a/cptr/frontend/src/lib/components/GitBar.svelte b/cptr/frontend/src/lib/components/GitBar.svelte index 4748422c..747fe16f 100644 --- a/cptr/frontend/src/lib/components/GitBar.svelte +++ b/cptr/frontend/src/lib/components/GitBar.svelte @@ -1,7 +1,6 @@ diff --git a/cptr/routers/events.py b/cptr/routers/events.py index 5849fdf0..2ce01d08 100644 --- a/cptr/routers/events.py +++ b/cptr/routers/events.py @@ -33,7 +33,17 @@ def _create_observer(): if platform.system() == "Darwin": - return PollingObserver() + # Prefer the native FSEvents observer on macOS – it fires only on + # real filesystem changes instead of polling (which generates + # constant false-positive "modified" events from stat calls). + try: + from watchdog.observers.fsevents import FSEventsObserver + + return FSEventsObserver() + except ImportError: + # If the C-extension isn't available, fall back to polling + # with a longer interval to reduce noise. + return PollingObserver(timeout=3) return Observer() @@ -44,8 +54,15 @@ def __init__(self) -> None: self._changes: set[str] = set() self._lock = threading.Lock() + # Paths that should never trigger a user-visible refresh + _IGNORED_SEGMENTS = {".git", "__pycache__", ".DS_Store", "node_modules"} + def _record(self, event: FileSystemEvent) -> None: path = event.src_path + # Skip noisy internal paths that shouldn't trigger file browser refreshes + parts = Path(path).parts + if any(seg in self._IGNORED_SEGMENTS for seg in parts): + return with self._lock: self._changes.add(str(Path(path).parent)) self._changes.add(path) @@ -57,6 +74,11 @@ def on_deleted(self, event: FileSystemEvent) -> None: self._record(event) def on_modified(self, event: FileSystemEvent) -> None: + # Ignore directory-modified events – these fire on any child + # change (which we already capture via on_created/on_deleted) + # and on metadata-only updates like atime from stat calls. + if event.is_directory: + return self._record(event) def on_moved(self, event: FileSystemEvent) -> None: From fcb11b214f63c8c5394fafd8ae3627b16048923c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 6 Jun 2026 14:49:33 +0100 Subject: [PATCH 3/7] refac --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c7e42d..10f3d1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.1] - 2026-06-06 + +### Fixed + +- 🛑 **File browser no longer refreshes constantly.** Replaced the macOS `PollingObserver` (which generated a feedback loop of phantom filesystem events) with the native `FSEventsObserver`. Background refreshes are now silent — no more loading spinner flash on every update. +- 🔇 **Eliminated noisy filesystem watcher events.** The watcher now ignores changes in `.git`, `__pycache__`, `.DS_Store`, and `node_modules` directories, which were triggering unnecessary file browser refreshes. +- ⚡ **Centralized git status store.** All components (GitBar, GitView, FileEditor, layout) now share a single `gitStatusStore` instead of each independently polling `git/status`. On page load, this reduces git status API calls from ~6+ down to 1. +- 🔁 **Reduced git polling frequency.** Removed the 5-second git status polling intervals that were running in multiple components simultaneously. + ## [0.1.0] - 2026-06-06 ### Added diff --git a/pyproject.toml b/pyproject.toml index 94e56617..fd5ca13f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cptr" -version = "0.1.0" +version = "0.1.1" description = "Your computer, from anywhere. Code, manage, and control your machine from the web." license = {file = "LICENSE"} readme = "README.md" From 48d49345de674198557ddb741ad7add2fb3d0b78 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 6 Jun 2026 14:52:53 +0100 Subject: [PATCH 4/7] refac --- .../lib/components/chat/FileSuggestionPopup.svelte | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cptr/frontend/src/lib/components/chat/FileSuggestionPopup.svelte b/cptr/frontend/src/lib/components/chat/FileSuggestionPopup.svelte index 70928f8d..0484a1e5 100644 --- a/cptr/frontend/src/lib/components/chat/FileSuggestionPopup.svelte +++ b/cptr/frontend/src/lib/components/chat/FileSuggestionPopup.svelte @@ -39,17 +39,17 @@ {#if items.length === 0}
No files found
{:else} -
+
Files
{#each items as item, i (item.id)}