feat: add Files tab for browsing workspace files (#154)

* feat: add Files tab for browsing workspace files

- Add Files tab between Diff and Terminal in header
- Implement hierarchical file tree with expand/collapse
- Add fuzzy search with debouncing and relevance ranking
- Support gitignore filtering via `git check-ignore` (web + desktop)
- Add syntax highlighting for 150+ file types
- Add image preview (SVG, PNG, JPG, etc.)
- Add line numbers, wrap toggle, and copy button
- Desktop: split-pane layout matching DiffView
- Mobile: drill-in navigation with full-width sidebar
- Update help dialog with Cmd+3 shortcut
- Increase header breakpoint to 940px for new tab

* feat: enhance context and session stores to track agent/model/variant choices for historical sessions

* feat: implement line selection and commenting functionality in FilesView
This commit is contained in:
Bohdan Triapitsyn
2026-01-15 20:19:06 +02:00
committed by GitHub
parent ecf81c901d
commit 1be5dfda05
21 changed files with 2070 additions and 82 deletions
+31 -3
View File
@@ -1,6 +1,11 @@
import { safeInvoke } from '../lib/tauriCallbackManager';
import type { DirectoryListResult, FileSearchQuery, FileSearchResult, FilesAPI } from '@openchamber/ui/lib/api/types';
import type { DirectoryListResult, FileSearchQuery, FileSearchResult, FilesAPI, ListDirectoryOptions } from '@openchamber/ui/lib/api/types';
type ReadFileBinaryResponse = {
dataUrl: string;
path: string;
};
type ListDirectoryResponse = DirectoryListResult & {
path?: string;
@@ -39,11 +44,12 @@ const normalizeDirectoryPayload = (result: ListDirectoryResponse): DirectoryList
});
export const createDesktopFilesAPI = (): FilesAPI => ({
async listDirectory(path: string): Promise<DirectoryListResult> {
async listDirectory(path: string, options?: ListDirectoryOptions): Promise<DirectoryListResult> {
try {
const result = await safeInvoke<ListDirectoryResponse>('list_directory', {
path: normalizePath(path),
includeHidden: false
includeHidden: false,
respectGitignore: options?.respectGitignore ?? false,
}, {
timeout: 10000,
onCancel: () => {
@@ -134,6 +140,28 @@ export const createDesktopFilesAPI = (): FilesAPI => ({
}
},
async readFileBinary(path: string): Promise<ReadFileBinaryResponse> {
try {
const normalizedPath = normalizePath(path);
const result = await safeInvoke<ReadFileBinaryResponse>('read_file_binary', {
path: normalizedPath
}, {
timeout: 15000,
onCancel: () => {
console.warn('[FilesAPI] Read binary file operation timed out');
}
});
return {
dataUrl: result?.dataUrl ?? '',
path: result?.path ? normalizePath(result.path) : normalizedPath,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(message || 'Failed to read file');
}
},
async writeFile(path: string, content: string): Promise<{ success: boolean; path: string }> {
try {
const normalizedPath = normalizePath(path);