FABRIC/src/focusPresets.ts
jeda 629d7430dc feat(bd-2u6): File Context Panel (Split View) implementation
Verified and closed bd-2u6 - FileContextPanel is fully implemented for both TUI and Web:

Features:
- Split view: Activity (left) | File Context (right)
- Auto-update when file event selected
- Syntax highlighting based on file extension (TS, JS, Python, Rust, Go, etc.)
- Line numbers with highlight on relevant lines
- Sticky panel - stays visible while scrolling activity
- File history: see all operations on this file
- Quick toggle between recently touched files ([/]/[] keys)
- 'Open in Editor' button (launches configured editor)

TUI Layout:
- Ctrl+F to toggle file context panel
- { and } to resize split view
- Tab to switch focus between panels

Web Layout:
- Collapsible side panel with toggle button
- Resizable divider
- Pop out to separate window

Editor Integration:
- Uses $EDITOR env var
- Supports VS Code, vim, nano, etc.
- Opens at specific line number

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Worker <noreply@anthropic.com>
2026-03-07 12:24:28 +00:00

266 lines
5.8 KiB
TypeScript

/**
* Focus Preset Management
*
* Provides save/load/delete functionality for focus mode pin configurations.
* Used by both TUI and Web interfaces.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
/**
* A saved focus mode configuration
*/
export interface FocusPreset {
/** Unique name for the preset */
name: string;
/** Pinned worker IDs */
pinnedWorkers: string[];
/** Pinned bead IDs */
pinnedBeads: string[];
/** Timestamp when preset was created */
createdAt: number;
/** Optional description */
description?: string;
}
/**
* Storage interface for presets
*/
export interface PresetStorage {
load(): FocusPreset[];
save(presets: FocusPreset[]): void;
}
/**
* In-memory storage (for testing)
*/
export class MemoryPresetStorage implements PresetStorage {
private presets: FocusPreset[] = [];
load(): FocusPreset[] {
return [...this.presets];
}
save(presets: FocusPreset[]): void {
this.presets = [...presets];
}
}
/**
* LocalStorage-based preset storage (for web)
* Note: This class is intended for browser environments only.
* For Node.js/TUI environments, use FilePresetStorage instead.
*/
export class LocalStoragePresetStorage implements PresetStorage {
private readonly key: string;
constructor(key = 'fabric-focus-presets') {
this.key = key;
}
load(): FocusPreset[] {
// This is a no-op in Node.js environments
// The web version has its own implementation
return [];
}
save(presets: FocusPreset[]): void {
// This is a no-op in Node.js environments
// The web version has its own implementation
}
}
/**
* File-based preset storage (for TUI)
*/
export class FilePresetStorage implements PresetStorage {
private readonly filePath: string;
constructor(filePath: string) {
this.filePath = filePath;
}
load(): FocusPreset[] {
try {
if (!fs.existsSync(this.filePath)) {
return [];
}
const data = fs.readFileSync(this.filePath, 'utf-8');
return JSON.parse(data) as FocusPreset[];
} catch (err) {
console.error('Failed to load presets from file:', err);
return [];
}
}
save(presets: FocusPreset[]): void {
try {
fs.writeFileSync(this.filePath, JSON.stringify(presets, null, 2));
} catch (err) {
console.error('Failed to save presets to file:', err);
}
}
}
/**
* FocusPresetManager handles CRUD operations for focus mode presets
*/
export class FocusPresetManager {
private storage: PresetStorage;
private presets: FocusPreset[];
private listeners: Set<() => void> = new Set();
constructor(storage: PresetStorage) {
this.storage = storage;
this.presets = this.storage.load();
}
/**
* Get all presets
*/
getPresets(): FocusPreset[] {
return [...this.presets];
}
/**
* Get preset by name
*/
getPreset(name: string): FocusPreset | undefined {
return this.presets.find(p => p.name === name);
}
/**
* Check if a preset exists
*/
hasPreset(name: string): boolean {
return this.presets.some(p => p.name === name);
}
/**
* Save current configuration as a new preset
*/
savePreset(
name: string,
pinnedWorkers: string[],
pinnedBeads: string[],
description?: string
): boolean {
// Check if name is valid
if (!name || name.trim() === '') {
return false;
}
const trimmedName = name.trim();
// Check for duplicate (update existing)
const existingIndex = this.presets.findIndex(p => p.name === trimmedName);
const preset: FocusPreset = {
name: trimmedName,
pinnedWorkers: [...pinnedWorkers],
pinnedBeads: [...pinnedBeads],
createdAt: existingIndex >= 0 ? this.presets[existingIndex].createdAt : Date.now(),
description,
};
if (existingIndex >= 0) {
this.presets[existingIndex] = preset;
} else {
this.presets.push(preset);
}
this.persist();
this.notifyListeners();
return true;
}
/**
* Delete a preset by name
*/
deletePreset(name: string): boolean {
const index = this.presets.findIndex(p => p.name === name);
if (index < 0) {
return false;
}
this.presets.splice(index, 1);
this.persist();
this.notifyListeners();
return true;
}
/**
* Load preset configuration (returns the pin data, doesn't apply it)
*/
loadPreset(name: string): { pinnedWorkers: string[]; pinnedBeads: string[] } | null {
const preset = this.getPreset(name);
if (!preset) {
return null;
}
return {
pinnedWorkers: [...preset.pinnedWorkers],
pinnedBeads: [...preset.pinnedBeads],
};
}
/**
* Get preset names as a list
*/
getPresetNames(): string[] {
return this.presets.map(p => p.name);
}
/**
* Subscribe to preset changes
*/
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
/**
* Persist presets to storage
*/
private persist(): void {
this.storage.save(this.presets);
}
/**
* Notify all listeners of changes
*/
private notifyListeners(): void {
this.listeners.forEach(listener => listener());
}
}
/**
* Create a preset manager for web (localStorage)
*/
export function createWebPresetManager(): FocusPresetManager {
return new FocusPresetManager(new LocalStoragePresetStorage());
}
/**
* Create a preset manager for TUI (file storage)
*/
export function createTuiPresetManager(configDir?: string): FocusPresetManager {
const dir = configDir || path.join(os.homedir(), '.fabric');
const filePath = path.join(dir, 'focus-presets.json');
return new FocusPresetManager(new FilePresetStorage(filePath));
}
/**
* Create a preset manager for testing (in-memory)
*/
export function createTestPresetManager(): FocusPresetManager {
return new FocusPresetManager(new MemoryPresetStorage());
}