refactor: remove auggie-specific code and use standard ACP protocol (#47)

- Remove ProtocolAuggie constant and AuggieAdapter
- Remove hooks system (HooksRegistry, AgentHooks, AuggieHooks)
- Remove PreGeneratedSessionID and SessionDir fields
- Change auggie-agent to use standard 'acp' protocol in agents.json
- Add model select dropdown to profile settings page
- Fix unused variable lint warnings

Auggie now uses the standard ACP protocol flow like other agents.
The --resume flag was found to not work in ACP mode, so the
workaround was unnecessary since session/new returns valid session IDs.
This commit is contained in:
João Salavisa
2026-01-19 22:41:00 +00:00
committed by GitHub
parent 8b8f8ff20b
commit 7e43534cd2
24 changed files with 556 additions and 345 deletions
@@ -1,44 +0,0 @@
{
"agents": [
{
"name": "codex",
"display_name": "Codex",
"supports_mcp": false,
"mcp_config_path": {
"linux": [],
"windows": [],
"macos": []
},
"installation_path": {
"linux": ["~/.codex/auth.json"],
"windows": ["~/.codex/auth.json"],
"macos": ["~/.codex/auth.json"]
},
"capabilities": {
"supports_session_resume": false,
"supports_shell": false,
"supports_workspace_only": false
}
},
{
"name": "auggie",
"display_name": "Auggie",
"supports_mcp": false,
"mcp_config_path": {
"linux": [],
"windows": [],
"macos": []
},
"installation_path": {
"linux": ["~/.augment/.auggie.json"],
"windows": ["~/.augment/.auggie.json"],
"macos": ["~/.augment/.auggie.json"]
},
"capabilities": {
"supports_session_resume": false,
"supports_shell": false,
"supports_workspace_only": false
}
}
]
}
@@ -1,32 +1,32 @@
// Package discovery provides agent installation detection and discovery functionality.
// It reads agent definitions from the unified agents.json configuration in the registry package.
package discovery
import (
"context"
"embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/kandev/kandev/internal/agent/registry"
)
//go:embed agents.json
var agentsFS embed.FS
// OSPaths defines OS-specific paths for discovery (re-exported from registry for API compatibility)
type OSPaths = registry.OSPaths
type OSPaths struct {
Linux []string `json:"linux"`
Windows []string `json:"windows"`
MacOS []string `json:"macos"`
}
// Capabilities defines discovery-specific capabilities (re-exported from registry)
type Capabilities = registry.DiscoveryCapabilities
type Capabilities struct {
SupportsSessionResume bool `json:"supports_session_resume"`
SupportsShell bool `json:"supports_shell"`
SupportsWorkspaceOnly bool `json:"supports_workspace_only"`
}
// ModelConfig defines the model configuration for an agent (re-exported from registry)
type ModelConfig = registry.ModelConfig
func (p OSPaths) ForOS(goos string) []string {
// ModelEntry defines a single model available for an agent (re-exported from registry)
type ModelEntry = registry.ModelEntry
// ForOS returns paths for the specified operating system
func ForOS(p OSPaths, goos string) []string {
switch goos {
case "windows":
return p.Windows
@@ -37,6 +37,7 @@ func (p OSPaths) ForOS(goos string) []string {
}
}
// KnownAgent represents an agent definition with discovery metadata
type KnownAgent struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
@@ -44,12 +45,15 @@ type KnownAgent struct {
MCPConfigPath OSPaths `json:"mcp_config_path"`
InstallationPath OSPaths `json:"installation_path"`
Capabilities Capabilities `json:"capabilities"`
ModelConfig ModelConfig `json:"model_config"`
}
// Config is the legacy structure for agent discovery configuration
type Config struct {
Agents []KnownAgent `json:"agents"`
}
// Availability represents the result of detecting an agent's installation
type Availability struct {
Name string `json:"name"`
SupportsMCP bool `json:"supports_mcp"`
@@ -59,29 +63,53 @@ type Availability struct {
MatchedPath string `json:"matched_path,omitempty"`
}
// Adapter defines the interface for agent detection strategies
type Adapter interface {
Detect(ctx context.Context) (Availability, error)
}
// Registry manages agent discovery adapters and definitions
type Registry struct {
adapters []Adapter
definitions []KnownAgent
}
// LoadRegistry creates a new discovery registry by loading agent definitions
// from the unified agents.json configuration in the registry package.
func LoadRegistry() (*Registry, error) {
data, err := agentsFS.ReadFile("agents.json")
agentConfigs, err := registry.GetAgentDefinitions()
if err != nil {
return nil, fmt.Errorf("read agents config: %w", err)
return nil, fmt.Errorf("load agent definitions: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse agents config: %w", err)
definitions := make([]KnownAgent, 0, len(agentConfigs))
adapters := make([]Adapter, 0, len(agentConfigs))
for _, agentCfg := range agentConfigs {
// Convert registry.AgentTypeConfig to discovery.KnownAgent
knownAgent := KnownAgent{
Name: extractAgentName(agentCfg.ID),
DisplayName: agentCfg.DisplayName,
SupportsMCP: agentCfg.Discovery.SupportsMCP,
MCPConfigPath: agentCfg.Discovery.MCPConfigPath,
InstallationPath: agentCfg.Discovery.InstallationPath,
Capabilities: agentCfg.Discovery.DiscoveryCapabilities,
ModelConfig: agentCfg.ModelConfig,
}
if knownAgent.DisplayName == "" {
knownAgent.DisplayName = agentCfg.Name
}
definitions = append(definitions, knownAgent)
adapters = append(adapters, NewFilePresenceAdapter(knownAgent))
}
adapters := make([]Adapter, 0, len(cfg.Agents))
for _, agent := range cfg.Agents {
adapters = append(adapters, NewFilePresenceAdapter(agent))
}
return &Registry{adapters: adapters, definitions: cfg.Agents}, nil
return &Registry{adapters: adapters, definitions: definitions}, nil
}
// extractAgentName extracts the short name from an agent ID (e.g., "auggie-agent" -> "auggie")
func extractAgentName(id string) string {
name := strings.TrimSuffix(id, "-agent")
return name
}
func (r *Registry) Definitions() []KnownAgent {
@@ -113,8 +141,8 @@ func NewFilePresenceAdapter(def KnownAgent) *FilePresenceAdapter {
func (a *FilePresenceAdapter) Detect(ctx context.Context) (Availability, error) {
_ = ctx
paths := resolvePaths(a.definition.InstallationPath.ForOS(runtime.GOOS))
mcpPaths := resolvePaths(a.definition.MCPConfigPath.ForOS(runtime.GOOS))
paths := resolvePaths(ForOS(a.definition.InstallationPath, runtime.GOOS))
mcpPaths := resolvePaths(ForOS(a.definition.MCPConfigPath, runtime.GOOS))
available, matched := anyPathExists(paths)
mcpPath := ""
if len(mcpPaths) > 0 {
@@ -1220,5 +1220,3 @@ func (m *Manager) RespondToPermissionBySessionID(sessionID, pendingID, optionID
return m.RespondToPermission(execution.ID, pendingID, optionID, cancelled)
}
@@ -5,8 +5,8 @@ import (
"context"
"time"
agentctl "github.com/kandev/kandev/internal/agentctl/client"
"github.com/kandev/kandev/internal/agent/registry"
agentctl "github.com/kandev/kandev/internal/agentctl/client"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
@@ -0,0 +1,202 @@
{
"$schema": "./agents.schema.json",
"version": "1.0",
"agents": [
{
"id": "auggie-agent",
"name": "Augment Coding Agent",
"display_name": "Auggie",
"description": "Auggie CLI-powered autonomous coding agent. Requires AUGMENT_SESSION_AUTH for authentication.",
"enabled": true,
"image": "kandev/augment-agent",
"tag": "latest",
"cmd": ["auggie", "--acp"],
"working_dir": "/workspace",
"required_env": ["AUGMENT_SESSION_AUTH"],
"env": {},
"mounts": [
{"source": "{workspace}", "target": "/workspace", "read_only": false}
],
"resource_limits": {
"memory_mb": 4096,
"cpu_cores": 2.0,
"timeout_seconds": 3600
},
"capabilities": ["code_generation", "code_review", "refactoring", "testing", "shell_execution"],
"protocol": "acp",
"protocol_config": {},
"model_flag": "--model",
"workspace_flag": "--workspace-root",
"session_config": {
"resume_via_acp": false,
"resume_flag": "",
"can_recover": false,
"session_dir_template": "{home}/.augment/sessions",
"session_dir_target": "/root/.augment/sessions"
},
"permission_config": {
"permission_flag": "--permission",
"tools_requiring_permission": [
"launch-process",
"save-file",
"str-replace-editor",
"remove-files"
]
},
"discovery": {
"supports_mcp": false,
"mcp_config_path": {
"linux": [],
"windows": [],
"macos": []
},
"installation_path": {
"linux": ["~/.augment/.auggie.json"],
"windows": ["~/.augment/.auggie.json"],
"macos": ["~/.augment/.auggie.json"]
},
"discovery_capabilities": {
"supports_session_resume": false,
"supports_shell": false,
"supports_workspace_only": false
}
},
"model_config": {
"default_model": "sonnet4.5",
"available_models": [
{
"id": "sonnet4.5",
"name": "Sonnet 4.5",
"provider": "anthropic",
"context_window": 200000,
"is_default": true
},
{
"id": "opus4.5",
"name": "Claude Opus 4.5",
"provider": "anthropic",
"context_window": 200000,
"is_default": false
},
{
"id": "haiku4.5",
"name": "Haiku 4.5",
"provider": "anthropic",
"context_window": 200000,
"is_default": false
},
{
"id": "sonnet4",
"name": "Sonnet 4",
"provider": "anthropic",
"context_window": 200000,
"is_default": false
},
{
"id": "gpt5.1",
"name": "GPT-5.1",
"provider": "openai",
"context_window": 128000,
"is_default": false
},
{
"id": "gpt5",
"name": "GPT-5",
"provider": "openai",
"context_window": 128000,
"is_default": false
}
]
}
},
{
"id": "codex-agent",
"name": "OpenAI Codex Agent",
"display_name": "Codex",
"description": "OpenAI Codex CLI-powered autonomous coding agent using the Codex app-server protocol.",
"enabled": true,
"image": "kandev/codex-agent",
"tag": "latest",
"cmd": ["codex", "app-server"],
"working_dir": "/workspace",
"required_env": [],
"env": {},
"mounts": [
{"source": "{workspace}", "target": "/workspace", "read_only": false}
],
"resource_limits": {
"memory_mb": 4096,
"cpu_cores": 2.0,
"timeout_seconds": 3600
},
"capabilities": ["code_generation", "code_review", "refactoring", "testing", "shell_execution"],
"protocol": "codex",
"protocol_config": {},
"model_flag": "--model",
"workspace_flag": "",
"session_config": {
"resume_via_acp": true,
"resume_flag": "",
"can_recover": true,
"session_dir_template": "{home}/.codex/sessions",
"session_dir_target": "/root/.codex/sessions"
},
"permission_config": {
"permission_flag": "",
"tools_requiring_permission": []
},
"discovery": {
"supports_mcp": false,
"mcp_config_path": {
"linux": [],
"windows": [],
"macos": []
},
"installation_path": {
"linux": ["~/.codex/auth.json"],
"windows": ["~/.codex/auth.json"],
"macos": ["~/.codex/auth.json"]
},
"discovery_capabilities": {
"supports_session_resume": false,
"supports_shell": false,
"supports_workspace_only": false
}
},
"model_config": {
"default_model": "gpt-5.2-codex",
"available_models": [
{
"id": "gpt-5.2-codex",
"name": "GPT-5.2 Codex",
"provider": "openai",
"context_window": 200000,
"is_default": true
},
{
"id": "gpt-5.1-codex-max",
"name": "GPT-5.1 Codex Max",
"provider": "openai",
"context_window": 200000,
"is_default": false
},
{
"id": "gpt-5.1-codex-mini",
"name": "GPT-5.1 Codex Mini",
"provider": "openai",
"context_window": 200000,
"is_default": false
},
{
"id": "gpt-5.2",
"name": "GPT-5.2",
"provider": "openai",
"context_window": 200000,
"is_default": false
}
]
}
}
]
}
@@ -1,184 +0,0 @@
package registry
import "github.com/kandev/kandev/pkg/agent"
// boolPtr returns a pointer to a bool value
func boolPtr(v bool) *bool {
return &v
}
// DefaultAgents returns the default agent configurations
func DefaultAgents() []*AgentTypeConfig {
return []*AgentTypeConfig{
{
ID: "auggie-agent",
Name: "Augment Coding Agent",
Description: "Auggie CLI-powered autonomous coding agent. Requires AUGMENT_SESSION_AUTH for authentication.",
Image: "kandev/augment-agent",
Tag: "latest",
Cmd: []string{"auggie", "--acp"},
WorkingDir: "/workspace",
RequiredEnv: []string{"AUGMENT_SESSION_AUTH"},
Env: map[string]string{},
Mounts: []MountTemplate{
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
},
ResourceLimits: ResourceLimits{
MemoryMB: 4096,
CPUCores: 2.0,
TimeoutSeconds: 3600,
},
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
Enabled: true,
ModelFlag: "--model",
WorkspaceFlag: "--workspace-root",
Protocol: agent.ProtocolACP,
SessionConfig: SessionConfig{
ResumeViaACP: false, // Auggie handles resume via CLI flag
ResumeFlag: "--resume",
CanRecover: boolPtr(false), // Auggie cannot recover sessions after backend restart
SessionDirTemplate: "{home}/.augment/sessions",
SessionDirTarget: "/root/.augment/sessions",
},
PermissionConfig: PermissionConfig{
PermissionFlag: "--permission",
ToolsRequiringPermission: []string{
"launch-process",
"save-file",
"str-replace-editor",
"remove-files",
},
},
},
{
ID: "claude-agent",
Name: "Claude Code Agent",
Description: "Claude Code CLI-powered autonomous coding agent.",
Image: "kandev/claude-agent",
Tag: "latest",
Cmd: []string{"claude", "--acp"},
WorkingDir: "/workspace",
RequiredEnv: []string{},
Env: map[string]string{},
Mounts: []MountTemplate{
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
},
ResourceLimits: ResourceLimits{
MemoryMB: 4096,
CPUCores: 2.0,
TimeoutSeconds: 3600,
},
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
Enabled: true,
ModelFlag: "--model",
Protocol: agent.ProtocolACP,
SessionConfig: SessionConfig{
ResumeViaACP: true, // Claude uses ACP session/load for resume
},
},
{
ID: "codex-agent",
Name: "OpenAI Codex Agent",
Description: "OpenAI Codex CLI-powered autonomous coding agent using the Codex app-server protocol.",
Image: "kandev/codex-agent",
Tag: "latest",
Cmd: []string{"codex", "app-server"},
WorkingDir: "/workspace",
RequiredEnv: []string{},
Env: map[string]string{},
Mounts: []MountTemplate{
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
},
ResourceLimits: ResourceLimits{
MemoryMB: 4096,
CPUCores: 2.0,
TimeoutSeconds: 3600,
},
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
Enabled: true,
ModelFlag: "--model",
Protocol: agent.ProtocolCodex,
SessionConfig: SessionConfig{
ResumeViaACP: true, // Codex uses thread/resume via ACP
},
},
{
ID: "gemini-agent",
Name: "Gemini CLI Agent",
Description: "Google Gemini CLI-powered autonomous coding agent.",
Image: "kandev/gemini-agent",
Tag: "latest",
Cmd: []string{"gemini", "--acp"},
WorkingDir: "/workspace",
RequiredEnv: []string{},
Env: map[string]string{},
Mounts: []MountTemplate{
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
},
ResourceLimits: ResourceLimits{
MemoryMB: 4096,
CPUCores: 2.0,
TimeoutSeconds: 3600,
},
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
Enabled: true,
ModelFlag: "--model",
Protocol: agent.ProtocolACP,
SessionConfig: SessionConfig{
ResumeViaACP: true, // Gemini uses ACP session/load for resume
},
},
{
ID: "opencode-agent",
Name: "OpenCode Agent",
Description: "OpenCode CLI-powered autonomous coding agent.",
Image: "kandev/opencode-agent",
Tag: "latest",
Cmd: []string{"opencode", "--acp"},
WorkingDir: "/workspace",
RequiredEnv: []string{},
Env: map[string]string{},
Mounts: []MountTemplate{
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
},
ResourceLimits: ResourceLimits{
MemoryMB: 4096,
CPUCores: 2.0,
TimeoutSeconds: 3600,
},
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
Enabled: true,
ModelFlag: "--model",
Protocol: agent.ProtocolACP,
SessionConfig: SessionConfig{
ResumeViaACP: true, // OpenCode uses ACP session/load for resume
},
},
{
ID: "copilot-agent",
Name: "GitHub Copilot Agent",
Description: "GitHub Copilot CLI-powered autonomous coding agent.",
Image: "kandev/copilot-agent",
Tag: "latest",
Cmd: []string{"copilot", "--acp"},
WorkingDir: "/workspace",
RequiredEnv: []string{},
Env: map[string]string{},
Mounts: []MountTemplate{
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
},
ResourceLimits: ResourceLimits{
MemoryMB: 4096,
CPUCores: 2.0,
TimeoutSeconds: 3600,
},
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
Enabled: true,
ModelFlag: "--model",
Protocol: agent.ProtocolACP,
SessionConfig: SessionConfig{
ResumeViaACP: true, // Copilot uses ACP session/load for resume
},
},
}
}
@@ -2,6 +2,7 @@
package registry
import (
"embed"
"encoding/json"
"fmt"
"os"
@@ -15,6 +16,15 @@ import (
v1 "github.com/kandev/kandev/pkg/api/v1"
)
//go:embed agents.json
var agentsFS embed.FS
// agentsConfig is the structure of the agents.json file
type agentsConfig struct {
Version string `json:"version"`
Agents []*AgentTypeConfig `json:"agents"`
}
// AgentTypeConfig holds configuration for an agent type
type AgentTypeConfig struct {
ID string `json:"id"`
@@ -43,6 +53,15 @@ type AgentTypeConfig struct {
// Permission configuration
PermissionConfig PermissionConfig `json:"permission_config,omitempty"` // How to handle tool permissions
// Discovery configuration (for detecting agent installation)
Discovery DiscoveryConfig `json:"discovery,omitempty"` // Discovery metadata for agent detection
// Model configuration
ModelConfig ModelConfig `json:"model_config,omitempty"` // Available models and default model
// Display name for UI (shorter than Name)
DisplayName string `json:"display_name,omitempty"`
}
// SessionConfig defines how session resumption is handled for an agent type
@@ -107,6 +126,43 @@ type ResourceLimits struct {
TimeoutSeconds int `json:"timeout_seconds"`
}
// ModelConfig defines the model configuration for an agent
type ModelConfig struct {
DefaultModel string `json:"default_model"`
AvailableModels []ModelEntry `json:"available_models"`
}
// ModelEntry defines a single model available for an agent
type ModelEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
ContextWindow int `json:"context_window"`
IsDefault bool `json:"is_default"`
}
// OSPaths defines OS-specific paths for discovery
type OSPaths struct {
Linux []string `json:"linux"`
Windows []string `json:"windows"`
MacOS []string `json:"macos"`
}
// DiscoveryCapabilities defines discovery-specific capabilities
type DiscoveryCapabilities struct {
SupportsSessionResume bool `json:"supports_session_resume"`
SupportsShell bool `json:"supports_shell"`
SupportsWorkspaceOnly bool `json:"supports_workspace_only"`
}
// DiscoveryConfig holds discovery-related configuration for an agent
type DiscoveryConfig struct {
SupportsMCP bool `json:"supports_mcp"`
MCPConfigPath OSPaths `json:"mcp_config_path"`
InstallationPath OSPaths `json:"installation_path"`
DiscoveryCapabilities DiscoveryCapabilities `json:"discovery_capabilities"`
}
// Registry manages agent type configurations
type Registry struct {
agents map[string]*AgentTypeConfig
@@ -311,3 +367,54 @@ func ValidateConfig(config *AgentTypeConfig) error {
return nil
}
// DefaultAgents returns the default agent configurations loaded from agents.json
func DefaultAgents() []*AgentTypeConfig {
agents, err := loadAgentsFromJSON()
if err != nil {
// Fall back to empty list if loading fails (should not happen with embedded file)
return []*AgentTypeConfig{}
}
return agents
}
// loadAgentsFromJSON loads agent configurations from the embedded agents.json file
func loadAgentsFromJSON() ([]*AgentTypeConfig, error) {
data, err := agentsFS.ReadFile("agents.json")
if err != nil {
return nil, fmt.Errorf("read agents config: %w", err)
}
var cfg agentsConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse agents config: %w", err)
}
// Post-process to convert protocol strings to agent.Protocol type
for _, agentCfg := range cfg.Agents {
agentCfg.Protocol = parseProtocol(string(agentCfg.Protocol))
}
return cfg.Agents, nil
}
// parseProtocol converts a protocol string to agent.Protocol
func parseProtocol(p string) agent.Protocol {
switch p {
case "acp":
return agent.ProtocolACP
case "rest":
return agent.ProtocolREST
case "mcp":
return agent.ProtocolMCP
case "codex":
return agent.ProtocolCodex
default:
return agent.ProtocolACP // Default to ACP
}
}
// GetAgentDefinitions returns the raw agent definitions from agents.json for discovery.
// This provides access to discovery metadata that may be needed by the discovery package.
func GetAgentDefinitions() ([]*AgentTypeConfig, error) {
return loadAgentsFromJSON()
}
@@ -89,6 +89,18 @@ func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailabl
if displayName == "" {
displayName = def.Name
}
// Convert model entries
modelEntries := make([]dto.ModelEntryDTO, 0, len(def.ModelConfig.AvailableModels))
for _, model := range def.ModelConfig.AvailableModels {
modelEntries = append(modelEntries, dto.ModelEntryDTO{
ID: model.ID,
Name: model.Name,
Provider: model.Provider,
ContextWindow: model.ContextWindow,
IsDefault: model.IsDefault,
})
}
payload = append(payload, dto.AvailableAgentDTO{
Name: availability.Name,
DisplayName: displayName,
@@ -102,6 +114,10 @@ func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailabl
SupportsShell: def.Capabilities.SupportsShell,
SupportsWorkspaceOnly: def.Capabilities.SupportsWorkspaceOnly,
},
ModelConfig: dto.ModelConfigDTO{
DefaultModel: def.ModelConfig.DefaultModel,
AvailableModels: modelEntries,
},
UpdatedAt: now,
})
}
@@ -51,6 +51,19 @@ type AgentCapabilitiesDTO struct {
SupportsWorkspaceOnly bool `json:"supports_workspace_only"`
}
type ModelEntryDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
ContextWindow int `json:"context_window"`
IsDefault bool `json:"is_default"`
}
type ModelConfigDTO struct {
DefaultModel string `json:"default_model"`
AvailableModels []ModelEntryDTO `json:"available_models"`
}
type AvailableAgentDTO struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
@@ -60,6 +73,7 @@ type AvailableAgentDTO struct {
Available bool `json:"available"`
MatchedPath string `json:"matched_path,omitempty"`
Capabilities AgentCapabilitiesDTO `json:"capabilities"`
ModelConfig ModelConfigDTO `json:"model_config"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -28,7 +28,7 @@ type CreateInstanceRequest struct {
ID string `json:"id,omitempty"`
WorkspacePath string `json:"workspace_path"`
AgentCommand string `json:"agent_command,omitempty"`
Protocol string `json:"protocol,omitempty"` // Protocol adapter to use (acp, rest, mcp)
Protocol string `json:"protocol,omitempty"` // Protocol adapter to use (acp, rest, mcp, codex)
WorkspaceFlag string `json:"workspace_flag,omitempty"` // CLI flag for workspace path (e.g., "--workspace-root")
Env map[string]string `json:"env,omitempty"`
AutoStart bool `json:"auto_start,omitempty"`
@@ -109,6 +109,10 @@ type InstanceConfig struct {
// LogFormat for this instance
LogFormat string
// AgentType identifies the agent (e.g., "auggie", "codex", "claude")
// Used for agent-specific adapter selection
AgentType string
}
// Load loads the configuration from environment variables.
@@ -168,6 +172,9 @@ func (c *Config) NewInstanceConfig(port int, overrides *InstanceOverrides) *Inst
if overrides.ApprovalPolicy != "" {
cfg.ApprovalPolicy = overrides.ApprovalPolicy
}
if overrides.AgentType != "" {
cfg.AgentType = overrides.AgentType
}
}
// Parse agent command into args
@@ -189,6 +196,7 @@ type InstanceOverrides struct {
AutoStart *bool
Env []string
ApprovalPolicy string
AgentType string
}
// ParseCommand splits a command string into arguments
@@ -52,7 +52,7 @@ type CreateRequest struct {
// AgentCommand is an optional command to start the agent. If empty, a default is used.
AgentCommand string `json:"agent_command,omitempty"`
// Protocol is the protocol adapter to use (acp, codex). If empty, default is used.
// Protocol is the protocol adapter to use (acp, codex, auggie). If empty, default is used.
Protocol string `json:"protocol,omitempty"`
// WorkspaceFlag is the CLI flag for workspace path (e.g., "--workspace-root").
@@ -24,10 +24,9 @@ func NewController(svc *orchestrator.Service) *Controller {
func (c *Controller) GetStatus(ctx context.Context, req dto.GetStatusRequest) (dto.StatusResponse, error) {
status := c.service.GetStatus()
return dto.StatusResponse{
Running: status.Running,
ActiveAgents: status.ActiveAgents,
QueuedTasks: status.QueuedTasks,
MaxConcurrent: 0, // Not available from service Status; would need scheduler access
Running: status.Running,
ActiveAgents: status.ActiveAgents,
QueuedTasks: status.QueuedTasks,
}, nil
}
@@ -6,10 +6,9 @@ type GetStatusRequest struct{}
// StatusResponse is the response for orchestrator.status
type StatusResponse struct {
Running bool `json:"running"`
ActiveAgents int `json:"active_agents"`
QueuedTasks int `json:"queued_tasks"`
MaxConcurrent int `json:"max_concurrent"`
Running bool `json:"running"`
ActiveAgents int `json:"active_agents"`
QueuedTasks int `json:"queued_tasks"`
}
// GetQueueRequest is the request for orchestrator.queue
@@ -17,7 +17,6 @@ import (
// Common errors
var (
ErrMaxConcurrentReached = errors.New("maximum concurrent executions reached")
ErrNoAgentProfileID = errors.New("task has no agent_profile_id configured")
ErrExecutionNotFound = errors.New("execution not found")
ErrExecutionAlreadyRunning = errors.New("execution already running")
@@ -146,7 +145,6 @@ type Executor struct {
logger *logger.Logger
// Configuration
maxConcurrent int
retryLimit int
retryDelay time.Duration
worktreeEnabled bool // Whether to use Git worktrees for agent isolation
@@ -154,7 +152,6 @@ type Executor struct {
// ExecutorConfig holds configuration for the Executor
type ExecutorConfig struct {
MaxConcurrent int // Maximum concurrent agent executions
WorktreeEnabled bool // Whether to use Git worktrees for agent isolation
ShellPrefs ShellPreferenceProvider
}
@@ -165,16 +162,11 @@ type ShellPreferenceProvider interface {
// NewExecutor creates a new executor
func NewExecutor(agentManager AgentManagerClient, repo repository.Repository, log *logger.Logger, cfg ExecutorConfig) *Executor {
maxConcurrent := cfg.MaxConcurrent
if maxConcurrent <= 0 {
maxConcurrent = 5 // default
}
return &Executor{
agentManager: agentManager,
repo: repo,
shellPrefs: cfg.ShellPrefs,
logger: log.WithFields(zap.String("component", "executor")),
maxConcurrent: maxConcurrent,
retryLimit: 3,
retryDelay: 5 * time.Second,
worktreeEnabled: cfg.WorktreeEnabled,
@@ -223,14 +215,6 @@ func (e *Executor) Execute(ctx context.Context, task *v1.Task) (*TaskExecution,
// ExecuteWithProfile starts agent execution for a task using an explicit agent profile.
// The prompt parameter is the initial prompt to send to the agent.
func (e *Executor) ExecuteWithProfile(ctx context.Context, task *v1.Task, agentProfileID string, prompt string) (*TaskExecution, error) {
// Check if max concurrent limit is reached
if !e.CanExecute() {
e.logger.Warn("max concurrent executions reached",
zap.Int("max", e.maxConcurrent),
zap.Int("current", e.ActiveCount()))
return nil, ErrMaxConcurrentReached
}
if agentProfileID == "" {
e.logger.Error("task has no agent_profile_id configured", zap.String("task_id", task.ID))
return nil, ErrNoAgentProfileID
@@ -465,13 +449,6 @@ func (e *Executor) ResumeSession(ctx context.Context, session *models.TaskSessio
return nil, ErrExecutionNotFound
}
if !e.CanExecute() {
e.logger.Warn("max concurrent executions reached",
zap.Int("max", e.maxConcurrent),
zap.Int("current", e.ActiveCount()))
return nil, ErrMaxConcurrentReached
}
if session.AgentProfileID == "" {
e.logger.Error("task session has no agent_profile_id configured",
zap.String("task_id", session.TaskID),
@@ -896,9 +873,10 @@ func (e *Executor) ActiveCount() int {
return len(sessions)
}
// CanExecute returns true if there's capacity for another execution
// CanExecute returns true if there's capacity for another execution.
// Currently always returns true as there is no concurrent execution limit.
func (e *Executor) CanExecute() bool {
return e.ActiveCount() < e.maxConcurrent
return true
}
// UpdateProgressBySession updates the progress of an execution by session ID
@@ -25,7 +25,6 @@ var (
// SchedulerConfig holds scheduler configuration
type SchedulerConfig struct {
ProcessInterval time.Duration // How often to process the queue
MaxConcurrent int // Max concurrent agent executions
RetryLimit int // Max retries for failed tasks
RetryDelay time.Duration // Delay between retries
}
@@ -34,7 +33,6 @@ type SchedulerConfig struct {
func DefaultSchedulerConfig() SchedulerConfig {
return SchedulerConfig{
ProcessInterval: 5 * time.Second,
MaxConcurrent: 5,
RetryLimit: 3,
RetryDelay: 30 * time.Second,
}
@@ -50,7 +48,6 @@ type TaskRepository interface {
type QueueStatus struct {
QueuedTasks int
ActiveExecutions int
MaxConcurrent int
TotalProcessed int64
TotalFailed int64
}
@@ -107,8 +104,7 @@ func (s *Scheduler) Start(ctx context.Context) error {
s.mu.Unlock()
s.logger.Info("scheduler starting",
zap.Duration("process_interval", s.config.ProcessInterval),
zap.Int("max_concurrent", s.config.MaxConcurrent))
zap.Duration("process_interval", s.config.ProcessInterval))
s.wg.Add(1)
go s.processLoop(ctx)
@@ -162,7 +158,6 @@ func (s *Scheduler) GetQueueStatus() *QueueStatus {
return &QueueStatus{
QueuedTasks: s.queue.Len(),
ActiveExecutions: s.executor.ActiveCount(),
MaxConcurrent: s.config.MaxConcurrent,
TotalProcessed: atomic.LoadInt64(&s.totalProcessed),
TotalFailed: atomic.LoadInt64(&s.totalFailed),
}
@@ -130,7 +130,7 @@ func createTestLogger() *logger.Logger {
return log
}
func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Logger, maxConcurrent int) *executor.Executor {
func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Logger) *executor.Executor {
tmpDir := t.TempDir()
dbConn, err := db.OpenSQLite(filepath.Join(tmpDir, "test.db"))
if err != nil {
@@ -150,7 +150,6 @@ func createTestExecutor(t *testing.T, agentMgr *mockAgentManager, log *logger.Lo
}
})
cfg := executor.ExecutorConfig{
MaxConcurrent: maxConcurrent,
WorktreeEnabled: true,
}
return executor.NewExecutor(agentMgr, repo, log, cfg)
@@ -172,7 +171,7 @@ func TestNewScheduler(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -190,9 +189,6 @@ func TestDefaultSchedulerConfig(t *testing.T) {
if cfg.ProcessInterval != 5*time.Second {
t.Errorf("expected ProcessInterval = 5s, got %v", cfg.ProcessInterval)
}
if cfg.MaxConcurrent != 5 {
t.Errorf("expected MaxConcurrent = 5, got %d", cfg.MaxConcurrent)
}
if cfg.RetryLimit != 3 {
t.Errorf("expected RetryLimit = 3, got %d", cfg.RetryLimit)
}
@@ -205,7 +201,7 @@ func TestStartStop(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -231,7 +227,7 @@ func TestStartAlreadyRunning(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -251,7 +247,7 @@ func TestStopNotRunning(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -266,7 +262,7 @@ func TestEnqueueTask(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -286,7 +282,7 @@ func TestRemoveTask(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -308,7 +304,7 @@ func TestRemoveNonExistentTask(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -323,11 +319,10 @@ func TestGetQueueStatus(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
cfg := DefaultSchedulerConfig()
cfg.MaxConcurrent = 10
s := NewScheduler(q, exec, taskRepo, log, cfg)
_ = s.EnqueueTask(createTestTask("task-1", 5))
@@ -337,9 +332,6 @@ func TestGetQueueStatus(t *testing.T) {
if status.QueuedTasks != 2 {
t.Errorf("expected QueuedTasks = 2, got %d", status.QueuedTasks)
}
if status.MaxConcurrent != 10 {
t.Errorf("expected MaxConcurrent = 10, got %d", status.MaxConcurrent)
}
if status.ActiveExecutions != 0 {
t.Errorf("expected ActiveExecutions = 0, got %d", status.ActiveExecutions)
}
@@ -349,7 +341,7 @@ func TestHandleTaskCompleted(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -378,7 +370,7 @@ func TestPriorityOrdering(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -399,7 +391,7 @@ func TestIsRunning(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -423,7 +415,7 @@ func TestSchedulerWithContextCancellation(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
cfg := DefaultSchedulerConfig()
@@ -452,7 +444,7 @@ func TestEnqueueDuplicateTask(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
s := NewScheduler(q, exec, taskRepo, log, DefaultSchedulerConfig())
@@ -470,7 +462,7 @@ func TestRetryTaskExceedsLimit(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
cfg := DefaultSchedulerConfig()
@@ -504,7 +496,7 @@ func TestRetryTaskNotFound(t *testing.T) {
q := queue.NewTaskQueue(100)
agentMgr := newMockAgentManager()
log := createTestLogger()
exec := createTestExecutor(t, agentMgr, log, 5)
exec := createTestExecutor(t, agentMgr, log)
taskRepo := newTestTaskRepository()
cfg := DefaultSchedulerConfig()
@@ -114,7 +114,6 @@ func NewService(
// Create the executor with the agent manager client and repository for persistent sessions
execCfg := executor.ExecutorConfig{
MaxConcurrent: cfg.Scheduler.MaxConcurrent,
WorktreeEnabled: cfg.WorktreeEnabled,
ShellPrefs: shellPrefs,
}
@@ -8,6 +8,13 @@ import { Button } from '@kandev/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kandev/ui/select';
import { Separator } from '@kandev/ui/separator';
import { Switch } from '@kandev/ui/switch';
import { Textarea } from '@kandev/ui/textarea';
@@ -82,6 +89,12 @@ function AgentSetupForm({
const resolveDisplayName = (name: string) =>
availableAgents.find((item) => item.name === name)?.display_name ?? '';
// Get model config for the current agent
const currentAgentModelConfig = useMemo(() => {
const agent = availableAgents.find((item) => item.name === draftAgent.name);
return agent?.model_config ?? { default_model: '', available_models: [] };
}, [availableAgents, draftAgent.name]);
const isProfileDirty = (draft: DraftProfile, saved?: AgentProfile) => {
if (!saved) return true;
return (
@@ -311,13 +324,34 @@ function AgentSetupForm({
<div className="space-y-2">
<Label>Model</Label>
<Input
value={profile.model}
onChange={(event) =>
handleProfileChange(profile.id, { model: event.target.value })
}
placeholder="model-id"
/>
{currentAgentModelConfig.available_models.length > 0 ? (
<Select
value={profile.model || currentAgentModelConfig.default_model}
onValueChange={(value) => handleProfileChange(profile.id, { model: value })}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
{currentAgentModelConfig.available_models.map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.name}
{model.is_default && (
<span className="ml-2 text-muted-foreground">(default)</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={profile.model}
onChange={(event) =>
handleProfileChange(profile.id, { model: event.target.value })
}
placeholder="model-id"
/>
)}
</div>
<div className="space-y-2">
@@ -9,21 +9,24 @@ import { Button } from '@kandev/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
import { Input } from '@kandev/ui/input';
import { Label } from '@kandev/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import { Separator } from '@kandev/ui/separator';
import { Switch } from '@kandev/ui/switch';
import { Textarea } from '@kandev/ui/textarea';
import { useToast } from '@/components/toast-provider';
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
import { deleteAgentProfileAction, updateAgentProfileAction } from '@/app/actions/agents';
import type { Agent, AgentProfile } from '@/lib/types/http';
import type { Agent, AgentProfile, ModelConfig } from '@/lib/types/http';
import { useAppStore } from '@/components/state-provider';
import { useAvailableAgents } from '@/hooks/use-available-agents';
type ProfileEditorProps = {
agent: Agent;
profile: AgentProfile;
modelConfig: ModelConfig;
};
function ProfileEditor({ agent, profile }: ProfileEditorProps) {
function ProfileEditor({ agent, profile, modelConfig }: ProfileEditorProps) {
const { toast } = useToast();
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
const setSettingsAgents = useAppStore((state) => state.setSettingsAgents);
@@ -151,11 +154,32 @@ function ProfileEditor({ agent, profile }: ProfileEditorProps) {
<div className="space-y-2">
<Label>Model</Label>
<Input
value={draft.model}
onChange={(event) => setDraft({ ...draft, model: event.target.value })}
placeholder="model-id"
/>
{modelConfig.available_models.length > 0 ? (
<Select
value={draft.model || modelConfig.default_model}
onValueChange={(value) => setDraft({ ...draft, model: value })}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
{modelConfig.available_models.map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.name}
{model.is_default && (
<span className="ml-2 text-muted-foreground">(default)</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={draft.model}
onChange={(event) => setDraft({ ...draft, model: event.target.value })}
placeholder="model-id"
/>
)}
</div>
<div className="space-y-2">
@@ -225,6 +249,7 @@ export default function AgentProfilePage() {
const agentKey = decodeURIComponent(agentParam ?? '');
const profileId = profileParam ?? '';
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
const availableAgents = useAvailableAgents().items;
const agent = useMemo(() => {
return settingsAgents.find((item) => item.name === agentKey) ?? null;
@@ -234,6 +259,11 @@ export default function AgentProfilePage() {
return agent?.profiles.find((item) => item.id === profileId) ?? null;
}, [agent?.profiles, profileId]);
const modelConfig = useMemo(() => {
const availableAgent = availableAgents.find((item) => item.name === agent?.name);
return availableAgent?.model_config ?? { default_model: '', available_models: [] };
}, [availableAgents, agent?.name]);
if (!agent || !profile) {
return (
<Card>
@@ -252,6 +282,7 @@ export default function AgentProfilePage() {
key={profile.id}
agent={agent}
profile={profile}
modelConfig={modelConfig}
/>
);
}
@@ -31,7 +31,6 @@ import {
} from '@kandev/ui/sidebar';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@kandev/ui/collapsible';
import { useAppStore } from '@/components/state-provider';
import { useAvailableAgents } from '@/hooks/use-available-agents';
export function SettingsAppSidebar() {
const pathname = usePathname();
@@ -39,7 +38,6 @@ export function SettingsAppSidebar() {
const environments = useAppStore((state) => state.environments.items);
const executors = useAppStore((state) => state.executors.items);
const agents = useAppStore((state) => state.settingsAgents.items);
const availableAgents = useAvailableAgents().items;
return (
<Sidebar variant="inset">
+5 -2
View File
@@ -1,6 +1,6 @@
'use client';
import { useEffect } from 'react';
import { useLayoutEffect } from 'react';
import type { AppState } from '@/lib/state/store';
import { useAppStoreApi } from '@/components/state-provider';
@@ -11,7 +11,10 @@ type StateHydratorProps = {
export function StateHydrator({ initialState }: StateHydratorProps) {
const store = useAppStoreApi();
useEffect(() => {
// Use useLayoutEffect to hydrate state synchronously before child effects run.
// This ensures SSR-hydrated data is available before hooks like useSettingsData
// decide whether to fetch data.
useLayoutEffect(() => {
if (Object.keys(initialState).length) {
store.getState().hydrate(initialState);
}
+27 -3
View File
@@ -3,7 +3,7 @@
import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { useVirtualizer } from '@tanstack/react-virtual';
import { IconBrain, IconListCheck, IconLoader2, IconPaperclip } from '@tabler/icons-react';
import { IconBrain, IconCpu, IconListCheck, IconLoader2, IconPaperclip } from '@tabler/icons-react';
import { Button } from '@kandev/ui/button';
import {
DropdownMenu,
@@ -24,6 +24,7 @@ import { useLazyLoadMessages } from '@/hooks/use-lazy-load-messages';
import { useSession } from '@/hooks/use-session';
import { useTask } from '@/hooks/use-task';
import { useSessionMessages } from '@/hooks/use-session-messages';
import { useSettingsData } from '@/hooks/use-settings-data';
import { MessageRenderer } from '@/components/task/chat/message-renderer';
import { RunningIndicator } from '@/components/task/chat/messages/running-indicator';
import { TodoSummary } from '@/components/task/chat/todo-summary';
@@ -65,6 +66,22 @@ export function TaskChatPanel({
const isWorking = isStarting || session?.state === 'RUNNING';
const isAgentBusy = session?.state === 'CREATED' || session?.state === 'RUNNING';
// Ensure agent profile data is loaded (may not be hydrated from SSR in all navigation paths)
useSettingsData(true);
// Get model from agent profile using agent_profile_id
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
const sessionProfile = useMemo(() => {
if (!session?.agent_profile_id) return null;
for (const agent of settingsAgents) {
const profile = agent.profiles.find((p) => p.id === session.agent_profile_id);
if (profile) return profile;
}
return null;
}, [session?.agent_profile_id, settingsAgents]);
const sessionModel = sessionProfile?.model ?? null;
// Fetch messages for this session
const { messages, isLoading: messagesLoading } = useSessionMessages(resolvedSessionId);
const isInitialLoading = messagesLoading && messages.length === 0;
@@ -305,10 +322,17 @@ export function TaskChatPanel({
)}
</div>
{/* Running indicator - shows agent state */}
{/* Session info - shows agent state and model */}
{session?.state && (
<div className="mt-2">
<div className="mt-2 flex items-center gap-2">
<RunningIndicator state={session.state} />
{/* Model indicator */}
{sessionModel && (
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-muted/50 text-xs text-muted-foreground border border-border/50">
<IconCpu className="h-3 w-3" />
<span className="font-medium">{sessionModel}</span>
</div>
)}
</div>
)}
+14
View File
@@ -376,6 +376,19 @@ export type AgentCapabilities = {
supports_workspace_only: boolean;
};
export type ModelEntry = {
id: string;
name: string;
provider: string;
context_window: number;
is_default: boolean;
};
export type ModelConfig = {
default_model: string;
available_models: ModelEntry[];
};
export type AvailableAgent = {
name: string;
display_name: string;
@@ -385,6 +398,7 @@ export type AvailableAgent = {
available: boolean;
matched_path?: string | null;
capabilities: AgentCapabilities;
model_config: ModelConfig;
updated_at: string;
};