feat: streaming agent install, PTY login terminal, docker container UX (#869)

This commit is contained in:
Carlos Florêncio
2026-05-11 17:52:05 +01:00
committed by GitHub
parent 6139dce03e
commit f97ac8bbc1
48 changed files with 4049 additions and 218 deletions
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/kandev/kandev/internal/agent/discovery"
agentdto "github.com/kandev/kandev/internal/agent/dto"
@@ -14,6 +15,7 @@ import (
"github.com/kandev/kandev/internal/agent/settings/modelfetcher"
"github.com/kandev/kandev/internal/agent/settings/store"
"github.com/kandev/kandev/internal/common/logger"
ws "github.com/kandev/kandev/pkg/websocket"
"go.uber.org/zap"
)
@@ -50,6 +52,8 @@ type Controller struct {
mcpService *mcpconfig.Service
modelCache *modelfetcher.Cache
hostUtility *hostutility.Manager
jobStore *JobStore
hub JobBroadcaster
logger *logger.Logger
}
@@ -88,3 +92,59 @@ func NewController(repo store.Repository, discoveryRegistry *discovery.Registry,
func (c *Controller) SetHostUtility(h *hostutility.Manager) {
c.hostUtility = h
}
// SetJobBroadcaster initializes the install job store with a WS broadcaster
// for streaming install progress. Called once during handler registration.
// If unset (hub == nil), the streaming install API returns
// ErrJobStoreUnavailable — without this guard a nil hub would silently
// degrade to a non-broadcasting store and the UI would never see progress.
func (c *Controller) SetJobBroadcaster(hub JobBroadcaster) {
c.hub = hub
if hub == nil {
c.jobStore = nil
return
}
c.jobStore = NewJobStore(hub, c.logger.Zap(), func(agentName string) {
c.InvalidateDiscoveryCache()
// Kick a fresh capability probe immediately so the UI doesn't sit on
// stale "not_installed" until the next periodic poll. When the probe
// finishes, re-broadcast the updated availability so any open profile
// page transitions out of "Probing…" without a manual refresh.
if c.hostUtility != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if _, err := c.hostUtility.Refresh(ctx, agentName); err != nil {
c.logger.Debug("post-install capability refresh failed",
zap.String("agent", agentName), zap.Error(err))
}
c.BroadcastAvailableAgents()
}()
}
c.logger.Info("install succeeded", zap.String("agent", agentName))
})
}
// BroadcastAvailableAgents fetches the current available-agents snapshot and
// pushes it over WS as `agent.available.updated`. Used after install + probe
// so the UI flips from "probing" to the resolved status without a refresh.
func (c *Controller) BroadcastAvailableAgents() {
if c.hub == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := c.ListAvailableAgents(ctx)
if err != nil {
c.logger.Debug("broadcast available agents: list failed", zap.Error(err))
return
}
msg, err := ws.NewNotification(ws.ActionAgentAvailableUpdated, map[string]any{
"agents": resp.Agents,
"tools": resp.Tools,
})
if err != nil {
return
}
c.hub.Broadcast(msg)
}