diff --git a/README.md b/README.md index 0303b54..50fe6d8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 - 업데이트: 2026-04-06 15:31 (KST) - 코워크/코드 하단 우측의 권한 요청 버튼은 footer 작업 바와 더 자연스럽게 이어지도록 외곽 테두리를 제거해 플랫한 형태로 정리했습니다. +- 업데이트: 2026-04-06 18:46 (KST) +- 런처 색인에서 `.git`, `node_modules`, `bin`, `obj`, `dist`, `packages`, `venv`, `__pycache__`, `target` 같은 무거운 폴더를 스캔/감시 대상에서 제외해 유휴 CPU와 재색인 부담을 줄였습니다. +- 런처 무지개 글로우 갱신 주기를 낮추고, AX Agent는 창 크기 변화 시 메시지 전체 재렌더를 짧게 묶어서 한 번만 반영하도록 바꿔 창 조작 시 버벅이는 느낌을 줄였습니다. + - 업데이트: 2026-04-06 15:26 (KST) - AX Agent 채팅/코워크/코드 하단 안내 문구를 현재 구현 기준으로 다시 정리했습니다. 입력창 워터마크는 탭 종류와 작업 폴더 선택 여부에 따라 실제 가능한 작업을 더 정확히 안내합니다. - 선택된 프리셋 안내도 placeholder 문구 대신 실제 설명 중심으로 정리해, 프리셋 설명 카드와 입력창 워터마크가 서로 다른 역할로 보이도록 맞췄습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index c9fdf53..b85acd4 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,5 +1,8 @@ # AX Copilot - 媛쒕컻 臾몄꽌 +- Document update: 2026-04-06 18:46 (KST) - Tightened launcher indexing for real-world projects by ignoring `.git`, `.vs`, `node_modules`, `bin`, `obj`, `dist`, `packages`, `venv`, `__pycache__`, and `target` directories during both watcher handling and full scans. The launcher now skips noisy build/cache trees instead of reacting to them like searchable content. +- Document update: 2026-04-06 18:46 (KST) - Replaced the launcher's recursive `EnumerateFiles(..., AllDirectories)` walk with an ignore-aware manual traversal so large repos avoid descending into dependency and build folders, and slowed the rainbow glow timer from `20ms` to `40ms`. +- Document update: 2026-04-06 18:46 (KST) - Added a deferred responsive-layout timer to `ChatWindow` so AX Agent no longer calls `RenderMessages()` on every `SizeChanged` event. Resizing and layout changes are now coalesced into a single update after a short delay, reducing the heavy-window feel during agent-window manipulation. - Document update: 2026-04-06 15:31 (KST) - Removed the visible outer border from the Cowork/Code footer permission selector so it reads as a flatter inline action in the bottom work bar. - Document update: 2026-04-06 15:26 (KST) - Reworked bottom guidance copy for Chat/Cowork/Code around the actual AX Agent behavior. Input watermark text now comes from a shared helper that considers the active tab and whether a work folder is selected, instead of using overly broad static text. - Document update: 2026-04-06 15:26 (KST) - Adjusted the selected preset guide so it prefers concise preset descriptions over raw placeholder prompts, keeping the footer guide and the composer watermark in distinct roles. diff --git a/src/AxCopilot/Services/IndexService.cs b/src/AxCopilot/Services/IndexService.cs index eb5ec97..e239091 100644 --- a/src/AxCopilot/Services/IndexService.cs +++ b/src/AxCopilot/Services/IndexService.cs @@ -13,6 +13,21 @@ namespace AxCopilot.Services; /// public class IndexService : IDisposable { + private static readonly HashSet IgnoredDirectoryNames = new(StringComparer.OrdinalIgnoreCase) + { + ".git", + ".vs", + "node_modules", + "bin", + "obj", + "dist", + "packages", + "__pycache__", + ".venv", + "venv", + "target" + }; + private readonly SettingsService _settings; private readonly List _watchers = new(); private readonly object _timerLock = new(); @@ -190,6 +205,9 @@ public class IndexService : IDisposable private void OnWatcherEvent(object sender, FileSystemEventArgs e) { + if (ShouldIgnorePath(e.FullPath)) + return; + var rootPath = (sender as FileSystemWatcher)?.Path; if (TryApplyIncrementalChange(e.ChangeType, e.FullPath, rootPath)) return; @@ -199,6 +217,9 @@ public class IndexService : IDisposable private void OnWatcherRenamed(object sender, RenamedEventArgs e) { + if (ShouldIgnorePath(e.FullPath) && ShouldIgnorePath(e.OldFullPath)) + return; + var rootPath = (sender as FileSystemWatcher)?.Path; if (TryApplyIncrementalRename(e.OldFullPath, e.FullPath, rootPath)) return; @@ -276,6 +297,9 @@ public class IndexService : IDisposable private bool AddPathEntryIncrementally(string fullPath, string rootPath) { + if (ShouldIgnorePath(fullPath)) + return false; + var updated = false; lock (_indexLock) @@ -307,6 +331,9 @@ public class IndexService : IDisposable private bool RemovePathEntriesIncrementally(string fullPath) { + if (ShouldIgnorePath(fullPath)) + return false; + var normalizedPath = NormalizePath(fullPath); lock (_indexLock) { @@ -340,6 +367,9 @@ public class IndexService : IDisposable private void ScheduleRebuild(string triggerPath) { + if (ShouldIgnorePath(triggerPath)) + return; + LogService.Info($"파일 변경 감지: {triggerPath} — {RebuildDebounceMs}ms 후 전체 재빌드 예약"); lock (_timerLock) { @@ -686,7 +716,7 @@ public class IndexService : IDisposable try { var count = 0; - foreach (var file in Directory.EnumerateFiles(dir, "*.*", SearchOption.AllDirectories)) + foreach (var file in EnumerateFilesWithIgnoredDirectories(dir)) { ct.ThrowIfCancellationRequested(); @@ -704,7 +734,7 @@ public class IndexService : IDisposable { ct.ThrowIfCancellationRequested(); var name = Path.GetFileName(subDir); - if (name.StartsWith(".", StringComparison.Ordinal)) + if (name.StartsWith(".", StringComparison.Ordinal) || IgnoredDirectoryNames.Contains(name)) continue; entries.Add(CreateFolderEntry(subDir)); @@ -717,6 +747,78 @@ public class IndexService : IDisposable }, ct); } + private static IEnumerable EnumerateFilesWithIgnoredDirectories(string rootDir) + { + var pending = new Stack(); + pending.Push(rootDir); + + while (pending.Count > 0) + { + var current = pending.Pop(); + + IEnumerable files; + try + { + files = Directory.EnumerateFiles(current, "*.*", SearchOption.TopDirectoryOnly); + } + catch (UnauthorizedAccessException) + { + continue; + } + catch (DirectoryNotFoundException) + { + continue; + } + + foreach (var file in files) + yield return file; + + IEnumerable directories; + try + { + directories = Directory.EnumerateDirectories(current, "*", SearchOption.TopDirectoryOnly); + } + catch (UnauthorizedAccessException) + { + continue; + } + catch (DirectoryNotFoundException) + { + continue; + } + + foreach (var directory in directories) + { + if (ShouldIgnorePath(directory)) + continue; + + pending.Push(directory); + } + } + } + + private static bool ShouldIgnorePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return false; + + try + { + var normalized = NormalizePath(path); + foreach (var segment in normalized.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) + { + if (IgnoredDirectoryNames.Contains(segment)) + return true; + } + } + catch + { + return false; + } + + return false; + } + private static IndexEntry CreateFileEntry(string filePath) { var ext = Path.GetExtension(filePath).ToLowerInvariant(); diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 2d9e4d5..32f51ba 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -93,6 +93,7 @@ public partial class ChatWindow : Window private readonly DispatcherTimer _conversationPersistTimer; private readonly DispatcherTimer _agentUiEventTimer; private readonly DispatcherTimer _tokenUsagePopupCloseTimer; + private readonly DispatcherTimer _responsiveLayoutTimer; private CancellationTokenSource? _gitStatusRefreshCts; private int _displayedLength; // 현재 화면에 표시된 글자 수 private ResourceDictionary? _agentThemeDictionary; @@ -273,6 +274,14 @@ public partial class ChatWindow : Window _tokenUsagePopupCloseTimer.Stop(); CloseTokenUsagePopupIfIdle(); }; + _responsiveLayoutTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(90) }; + _responsiveLayoutTimer.Tick += (_, _) => + { + _responsiveLayoutTimer.Stop(); + UpdateTopicPresetScrollMode(); + if (UpdateResponsiveChatLayout()) + RenderMessages(preserveViewport: true); + }; KeyDown += ChatWindow_KeyDown; MouseMove += (_, _) => @@ -401,9 +410,8 @@ public partial class ChatWindow : Window return; } - UpdateTopicPresetScrollMode(); - if (UpdateResponsiveChatLayout()) - RenderMessages(preserveViewport: true); + _responsiveLayoutTimer.Stop(); + _responsiveLayoutTimer.Start(); }; Closed += (_, _) => { @@ -415,6 +423,7 @@ public partial class ChatWindow : Window _typingTimer.Stop(); _conversationSearchTimer.Stop(); _inputUiRefreshTimer.Stop(); + _responsiveLayoutTimer.Stop(); _llm.Dispose(); }; } diff --git a/src/AxCopilot/Views/LauncherWindow.xaml.cs b/src/AxCopilot/Views/LauncherWindow.xaml.cs index 3d73bd1..4aa7623 100644 --- a/src/AxCopilot/Views/LauncherWindow.xaml.cs +++ b/src/AxCopilot/Views/LauncherWindow.xaml.cs @@ -617,7 +617,7 @@ public partial class LauncherWindow : Window _rainbowTimer = new System.Windows.Threading.DispatcherTimer { - Interval = TimeSpan.FromMilliseconds(20) + Interval = TimeSpan.FromMilliseconds(40) }; var startTime = DateTime.UtcNow; _rainbowTimer.Tick += (_, _) =>