feat: Interactive shell terminal for agent tasks (#19)

This commit is contained in:
João Salavisa
2026-01-16 01:54:06 +00:00
committed by GitHub
parent e08fa51684
commit f86044c083
23 changed files with 2119 additions and 41 deletions
@@ -118,6 +118,9 @@ type Manager struct {
agentCfg config.AgentConfig
standaloneCtl *agentctl.StandaloneCtl // Client for standalone agentctl control API
// Shell stream starter for auto-starting shell streams
shellStreamStarter ShellStreamStarter
// Track active instances
instances map[string]*AgentInstance // by instance ID
byTask map[string]string // taskID -> instanceID
@@ -186,6 +189,16 @@ func (m *Manager) SetWorktreeManager(worktreeMgr *worktree.Manager) {
m.worktreeMgr = worktreeMgr
}
// ShellStreamStarter is called to start shell streaming for an agent instance
type ShellStreamStarter interface {
StartShellStream(ctx context.Context, taskID string) error
}
// SetShellStreamStarter sets the shell stream starter
func (m *Manager) SetShellStreamStarter(starter ShellStreamStarter) {
m.shellStreamStarter = starter
}
// Start starts the lifecycle manager background tasks
func (m *Manager) Start(ctx context.Context) error {
m.logger.Info("starting lifecycle manager",
@@ -836,6 +849,10 @@ func (m *Manager) initializeACPSession(ctx context.Context, instance *AgentInsta
// 6. Set up file changes stream to track filesystem changes
go m.handleFileChangesStream(instance)
// Note: Shell stream is started on-demand when a client sends shell.subscribe
// This ensures the buffered output (including initial prompt) is received
// when a client is actually listening
// Wait for the updates stream to connect before sending prompt
// This prevents race conditions where notifications are sent before streams are ready
select {
@@ -1004,6 +1021,7 @@ func (m *Manager) reconnectStreams(instance *AgentInstance) {
go m.handlePermissionStream(instance)
go m.handleGitStatusStream(instance)
go m.handleFileChangesStream(instance)
// Note: Shell stream is started on-demand via shell.subscribe
// Mark the instance as READY so it can accept prompts
m.mu.Lock()