FABRIC/src/tui/components/ActivityStream.ts
jeda 73f8eb2616 feat(bd-k1p): Add Focus Mode UI controls
Implemented Focus Mode functionality with the following features:
- Added keybindings: p (pin/unpin worker), P (pin/unpin bead), F (toggle focus)
- Visual indicators (📌) for pinned workers and beads
- Dimmed display for non-pinned items when Focus Mode is enabled
- Updated footer to show Focus Mode status and pinned items
- Updated help overlay with Focus Mode documentation
- Added setFocusMode methods to WorkerGrid and ActivityStream components

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-04 04:12:57 +00:00

292 lines
6.7 KiB
TypeScript

/**
* ActivityStream Component
*
* Displays scrolling log output with filtering capabilities.
*/
import * as blessed from 'blessed';
import { LogEvent } from '../../types.js';
import { colors, getLevelColor } from '../utils/colors.js';
export interface ActivityStreamOptions {
/** Parent screen */
parent: blessed.Widgets.Screen;
/** Position from top */
top: number | string;
/** Position from right */
right: number | string;
/** Width of the panel */
width: number | string;
/** Position from bottom */
bottom: number | string;
/** Maximum lines to keep in buffer */
maxLines?: number;
}
export interface ActivityFilter {
/** Filter by worker ID */
workerId?: string;
/** Filter by log level */
level?: string;
/** Filter by search term */
search?: string;
/** Filter by time range start (Unix timestamp in ms) */
since?: number;
/** Filter by time range end (Unix timestamp in ms) */
until?: number;
}
/**
* ActivityStream displays real-time log events
*/
export class ActivityStream {
private log: blessed.Widgets.Log;
private events: LogEvent[] = [];
private filter: ActivityFilter = {};
private maxLines: number;
private isPaused = false;
private focusModeEnabled = false;
private pinnedBeadId?: string;
private pinnedWorkerId?: string;
constructor(options: ActivityStreamOptions) {
this.maxLines = options.maxLines || 500;
this.log = blessed.log({
parent: options.parent,
top: options.top,
right: options.right,
width: options.width,
bottom: options.bottom,
label: ' Activity Stream ',
border: { type: 'line' },
style: {
border: { fg: colors.border },
label: { fg: colors.header },
},
scrollable: true,
alwaysScroll: true,
keys: true,
vi: true,
mouse: true,
});
this.bindKeys();
}
/**
* Bind component-specific keys
*/
private bindKeys(): void {
this.log.key(['p'], () => {
this.togglePause();
});
this.log.key(['C-c'], () => {
this.clear();
});
}
/**
* Format event for display
*/
private formatEvent(event: LogEvent): string {
const time = new Date(event.ts).toLocaleTimeString();
const levelColor = getLevelColor(event.level as 'debug' | 'info' | 'warn' | 'error');
const workerShort = event.worker.slice(0, 8);
let msg = event.msg;
if (event.tool) {
msg = `[${event.tool}] ${msg}`;
}
// Check if this event is pinned
const isBeadPinned = this.pinnedBeadId && event.bead === this.pinnedBeadId;
const isWorkerPinned = this.pinnedWorkerId && event.worker === this.pinnedWorkerId;
const isPinned = isBeadPinned || isWorkerPinned;
const pinIndicator = isPinned ? '{yellow-fg}📌{/}' : '';
if (event.bead) {
msg = `{blue-fg}${event.bead}{/} ${pinIndicator}${msg}`;
}
// Dim non-pinned events when in focus mode
const shouldDim = this.focusModeEnabled && (this.pinnedBeadId || this.pinnedWorkerId) && !isPinned;
const dimPrefix = shouldDim ? '{gray-fg}' : '';
const dimSuffix = shouldDim ? '{/}' : '';
return `${dimPrefix}{gray-fg}${time}{/} {bold}${workerShort}{/} {${levelColor}-fg}${event.level.toUpperCase()}{/} ${msg}${dimSuffix}`;
}
/**
* Check if event passes current filter
*/
private passesFilter(event: LogEvent): boolean {
if (this.filter.workerId && event.worker !== this.filter.workerId) {
return false;
}
if (this.filter.level && event.level !== this.filter.level) {
return false;
}
if (this.filter.since && event.ts < this.filter.since) {
return false;
}
if (this.filter.until && event.ts > this.filter.until) {
return false;
}
if (this.filter.search) {
const searchLower = this.filter.search.toLowerCase();
const matchesSearch =
event.msg.toLowerCase().includes(searchLower) ||
event.worker.toLowerCase().includes(searchLower) ||
(event.tool?.toLowerCase().includes(searchLower) ?? false) ||
(event.bead?.toLowerCase().includes(searchLower) ?? false);
if (!matchesSearch) {
return false;
}
}
return true;
}
/**
* Add event to the stream
*/
addEvent(event: LogEvent): void {
this.events.push(event);
// Trim old events
if (this.events.length > this.maxLines) {
this.events = this.events.slice(-this.maxLines);
}
// Only display if not paused and passes filter
if (!this.isPaused && this.passesFilter(event)) {
const formatted = this.formatEvent(event);
this.log.log(formatted);
}
}
/**
* Add multiple events
*/
addEvents(events: LogEvent[]): void {
for (const event of events) {
this.addEvent(event);
}
}
/**
* Toggle pause state
*/
togglePause(): void {
this.isPaused = !this.isPaused;
const label = this.isPaused ? ' Activity Stream [PAUSED] ' : ' Activity Stream ';
this.log.setLabel(label);
this.log.screen.render();
}
/**
* Set filter and re-render
*/
setFilter(filter: ActivityFilter): void {
this.filter = filter;
this.reRender();
}
/**
* Clear filter
*/
clearFilter(): void {
this.filter = {};
this.reRender();
}
/**
* Re-render all events with current filter
*/
private reRender(): void {
// Clear the log
this.log.setContent('');
// Re-add filtered events
const filtered = this.events.filter(e => this.passesFilter(e));
for (const event of filtered.slice(-100)) { // Show last 100 matching
const formatted = this.formatEvent(event);
this.log.log(formatted);
}
this.log.screen.render();
}
/**
* Clear all events
*/
clear(): void {
this.events = [];
this.log.setContent('');
this.log.screen.render();
}
/**
* Focus this component
*/
focus(): void {
this.log.focus();
}
/**
* Get the underlying log element
*/
getElement(): blessed.Widgets.Log {
return this.log;
}
/**
* Get pause state
*/
getIsPaused(): boolean {
return this.isPaused;
}
/**
* Get current filter
*/
getFilter(): ActivityFilter {
return { ...this.filter };
}
/**
* Get current events count
*/
getEventsCount(): number {
return this.events.length;
}
/**
* Get filtered events count
*/
getFilteredEventsCount(): number {
return this.events.filter(e => this.passesFilter(e)).length;
}
/**
* Set focus mode state
*/
setFocusMode(enabled: boolean, pinnedBeadId?: string, pinnedWorkerId?: string): void {
this.focusModeEnabled = enabled;
this.pinnedBeadId = pinnedBeadId;
this.pinnedWorkerId = pinnedWorkerId;
this.reRender();
}
}
export default ActivityStream;