feat(web): File browser with syntax-highlighted viewer (#13)
* feat(web): add file browser with syntax-highlighted viewer - Add file browser component in right panel to browse workspace files - Open files in left panel tabs alongside Notes/Changes/Chat - Color-coded dots for file types (TS, JS, Go, Python, etc.) - Visual separator between main tabs and file tabs - Folders sorted to top in file tree view - LRU eviction for max 4 open file tabs - Backend handlers for workspace file tree and content via WebSocket * fix(web): refresh file tree on filesystem changes via fsnotify - Subscribe to file change notifications from agentctl's fsnotify watcher - Backend lifecycle manager now connects to file changes stream - Publishes FileChangeNotified events to event bus - WebSocket broadcasts workspace.file.changes to task subscribers - Frontend file browser refreshes tree when changes detected * perf(agentctl): use fsnotify instead of polling for git status - Replace 2-second polling with fsnotify-triggered updates - Add debouncing (300ms) to batch rapid file changes - Git status updates now triggered by actual filesystem changes - More efficient and responsive than periodic polling * refactor(agentctl): consolidate fs events into single debounced trigger - Single fsChangeTrigger channel for all filesystem events - After 300ms debounce: update git status, files, and notify subscribers - Removed per-file notifications in favor of single 'refresh' event - Simpler and more efficient than separate triggers * refactor(agentctl): simplify file API - use HTTP for tree/content - Remove unused filesConn, fileTreeConn, fileContentConn from Client struct - Convert RequestFileTree and RequestFileContent from WebSocket to HTTP GET - Remove StreamFiles and CloseFilesStream (redundant with file changes stream) - Keep only fileChangesConn for the debounced refresh notification HTTP is simpler than WebSocket for request/response patterns. * feat(web): switch to CodeMirror for file viewer - Replace react-syntax-highlighter with @uiw/react-codemirror - Add language extensions for JS/TS, Python, Go, Rust, Java, C++, CSS, HTML, JSON, Markdown, YAML - Use VS Code dark theme for consistent look - Enable code folding and read-only mode - Better performance and features than syntax highlighter * refactor: consolidate duplicated types and extract shared constants - Re-export file types from process package in agentctl/client.go instead of duplicating - Move OpenFileTab type to lib/types/backend.ts (shared by file-browser and page-client) - Extract FILE_EXTENSION_COLORS constant for file type color indicators - Remove console.log statement and unused imports in file-browser.tsx - Add 'refresh' operation to FileChangeNotificationPayload type * refactor(agentctl): move Close() method from acp.go to client.go Close() is a general client cleanup method that closes all streams (ACP, permissions, git status, file changes). It belongs in client.go since acp.go should only contain ACP-specific communication logic. * fix(web): remove unused cn import in file-browser * refactor(agentctl): consolidate duplicated types into process/types.go - Create process/types.go with all workspace-related types - Remove duplicate GitStatusUpdate, FileInfo from workspace_tracker.go - Remove duplicate GitStatusUpdate, FileInfo from api/workspace.go - Re-export all types from agent/agentctl/client.go for convenience Types are now defined once and imported everywhere. * chore: remove package-lock.json, add to .gitignore Project uses pnpm (pnpm-lock.yaml), not npm. * fix(orchestrator): probe agent before returning stale execution state When checking task.execution, the backend now probes the actual agent: - For tracked instances: health check via agentctl - For Docker: verify container is running If the agent is not running but DB says 'running', the session is marked as stopped and false is returned. This fixes the UI showing agents as running after backend restart when they're actually dead. Changes: - Add IsAgentRunningForTask to AgentManagerClient interface - Add IsContainerRunning to docker client - Update GetExecution to verify agent is actually running - Clean up stale sessions in database when agent not found * fix(orchestrator): use agentctl status to verify agent is running Simplified approach - probe agentctl's /api/v1/status endpoint to check if agent process is running. Same check for both Docker and standalone. - Add IsAgentRunning() helper to StatusResponse - Simplify IsAgentRunningForTask to just use agentctl.GetStatus() * fix(orchestrator): also verify agent running for in-memory executions The previous fix only checked agent status when loading from DB. But if an execution is cached in memory and the agent process stops (while agentctl stays running), the stale state was still returned. Now both paths verify the agent is actually running by probing agentctl. If agent stopped, clean up in-memory state and update DB session. * chore(web): remove unused react-syntax-highlighter Switched to CodeMirror for code viewing, no longer needed. * fix(tests): add IsAgentRunningForTask to mock agent managers Add the new interface method to test mocks: - mockAgentManager in scheduler_test.go - SimulatedAgentManagerClient in orchestrator_test.go --------- Co-authored-by: Kandev Agent <kandev@example.com>
This commit is contained in:
@@ -57,6 +57,11 @@ type AgentInstance struct {
|
||||
messageMu sync.Mutex
|
||||
}
|
||||
|
||||
// GetAgentCtlClient returns the agentctl client for this instance
|
||||
func (ai *AgentInstance) GetAgentCtlClient() *agentctl.Client {
|
||||
return ai.agentctl
|
||||
}
|
||||
|
||||
// LaunchRequest contains parameters for launching an agent
|
||||
type LaunchRequest struct {
|
||||
TaskID string
|
||||
@@ -740,7 +745,10 @@ func (m *Manager) initializeACPSession(ctx context.Context, instance *AgentInsta
|
||||
// 5. Set up git status stream to track workspace changes
|
||||
go m.handleGitStatusStream(instance)
|
||||
|
||||
// 5. Send the task prompt if provided
|
||||
// 6. Set up file changes stream to track filesystem changes
|
||||
go m.handleFileChangesStream(instance)
|
||||
|
||||
// 7. Send the task prompt if provided
|
||||
if taskDescription != "" {
|
||||
m.logger.Info("sending ACP prompt",
|
||||
zap.String("instance_id", instance.ID),
|
||||
@@ -885,6 +893,7 @@ func (m *Manager) reconnectStreams(instance *AgentInstance) {
|
||||
go m.handleUpdatesStream(instance)
|
||||
go m.handlePermissionStream(instance)
|
||||
go m.handleGitStatusStream(instance)
|
||||
go m.handleFileChangesStream(instance)
|
||||
|
||||
// Mark the instance as READY so it can accept prompts
|
||||
m.mu.Lock()
|
||||
@@ -1184,6 +1193,60 @@ func (m *Manager) publishGitStatus(instance *AgentInstance, update *agentctl.Git
|
||||
}
|
||||
}
|
||||
|
||||
// handleFileChangesStream handles streaming file change notifications from the agent workspace
|
||||
func (m *Manager) handleFileChangesStream(instance *AgentInstance) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Retry connection with exponential backoff
|
||||
maxRetries := 5
|
||||
backoff := 1 * time.Second
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
err := instance.agentctl.StreamFileChanges(ctx, func(notification *agentctl.FileChangeNotification) {
|
||||
m.publishFileChange(instance, notification)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
// Connection closed normally
|
||||
return
|
||||
}
|
||||
|
||||
if attempt < maxRetries {
|
||||
time.Sleep(backoff)
|
||||
backoff *= 2 // Exponential backoff
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Error("failed to connect to file changes stream after retries",
|
||||
zap.String("instance_id", instance.ID),
|
||||
zap.Int("max_retries", maxRetries))
|
||||
}
|
||||
|
||||
// publishFileChange publishes a file change notification to the event bus
|
||||
func (m *Manager) publishFileChange(instance *AgentInstance, notification *agentctl.FileChangeNotification) {
|
||||
if m.eventBus == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"task_id": instance.TaskID,
|
||||
"agent_id": instance.ID,
|
||||
"path": notification.Path,
|
||||
"operation": notification.Operation,
|
||||
"timestamp": notification.Timestamp.Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
event := bus.NewEvent(events.FileChangeNotified, "agent-manager", data)
|
||||
subject := events.BuildFileChangeSubject(instance.TaskID)
|
||||
|
||||
if err := m.eventBus.Publish(context.Background(), subject, event); err != nil {
|
||||
m.logger.Error("failed to publish file change event",
|
||||
zap.String("instance_id", instance.ID),
|
||||
zap.String("task_id", instance.TaskID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// publishPromptComplete publishes a prompt_complete event when an agent finishes responding
|
||||
// This is used to save the agent's response as a comment on the task
|
||||
func (m *Manager) publishPromptComplete(instance *AgentInstance, agentMessage string) {
|
||||
@@ -1714,6 +1777,31 @@ func (m *Manager) ListInstances() []*AgentInstance {
|
||||
return result
|
||||
}
|
||||
|
||||
// IsAgentRunningForTask checks if an agent is actually running for a task
|
||||
// This probes agentctl's status endpoint to verify the agent process is running
|
||||
func (m *Manager) IsAgentRunningForTask(ctx context.Context, taskID string) bool {
|
||||
// First check if we have an instance tracked for this task
|
||||
instance, exists := m.GetInstanceByTaskID(taskID)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Probe agentctl status to verify the agent process is running
|
||||
if instance.agentctl == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
status, err := instance.agentctl.GetStatus(ctx)
|
||||
if err != nil {
|
||||
m.logger.Debug("failed to get agentctl status",
|
||||
zap.String("task_id", taskID),
|
||||
zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
return status.IsAgentRunning()
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status of an instance
|
||||
func (m *Manager) UpdateStatus(instanceID string, status v1.AgentStatus) error {
|
||||
m.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user