• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / kvelmo / 22732077213

05 Mar 2026 06:45PM UTC coverage: 44.057% (-0.4%) from 44.449%
22732077213

push

github

k0d3r1s
Fix web UI undo/redo visibility and project/state refresh

Hides undo/redo buttons when no checkpoints exist and renders only the
applicable button instead of always showing both disabled. Fixes
ActiveTasksWidget to match projects by path only (avoids cross-project
task display). Calls refreshStatus after state_changed, task_abandoned,
task_deleted, and task_reset events so the UI stays in sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

139 of 471 branches covered (29.51%)

Branch coverage included in aggregate %.

0 of 6 new or added lines in 1 file covered. (0.0%)

351 existing lines in 9 files now uncovered.

10203 of 23003 relevant lines covered (44.36%)

0.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.85
/pkg/agent/codex/codex.go
1
// Package codex implements the Codex AI agent.
2
// Supports WebSocket (primary) and CLI (fallback) connection modes.
3
package codex
4

5
import (
6
        "context"
7
        "errors"
8
        "fmt"
9
        "os/exec"
10
        "sync"
11
        "time"
12

13
        "github.com/valksor/kvelmo/pkg/agent"
14
)
15

16
const AgentName = "codex"
17

18
// Agent wraps the Codex CLI with WebSocket and CLI modes.
19
type Agent struct {
20
        config Config
21
        mode   agent.ConnectionMode
22

23
        // WebSocket mode
24
        ws *WebSocketConnection
25

26
        // CLI mode
27
        cli *CLIConnection
28

29
        mu sync.RWMutex
30
}
31

32
// Config holds Codex agent configuration.
33
type Config struct {
34
        agent.Config
35

36
        // Model to use (e.g., "o1", "o3-mini")
37
        Model string
38
}
39

40
// DefaultConfig returns default Codex configuration.
41
func DefaultConfig() Config {
1✔
42
        cfg := Config{
1✔
43
                Config: agent.DefaultConfig(),
1✔
44
        }
1✔
45
        cfg.Command = []string{"codex"}
1✔
46

1✔
47
        return cfg
1✔
48
}
1✔
49

50
// New creates a Codex agent with default configuration.
51
func New() *Agent {
1✔
52
        return NewWithConfig(DefaultConfig())
1✔
53
}
1✔
54

55
// NewWithConfig creates a Codex agent with custom configuration.
56
func NewWithConfig(cfg Config) *Agent {
1✔
57
        if len(cfg.Command) == 0 {
2✔
58
                cfg.Command = []string{"codex"}
1✔
59
        }
1✔
60
        if cfg.Timeout == 0 {
2✔
61
                cfg.Timeout = 30 * time.Minute
1✔
62
        }
1✔
63
        if cfg.PermissionHandler == nil {
2✔
64
                cfg.PermissionHandler = agent.DefaultPermissionHandler
1✔
65
        }
1✔
66

67
        return &Agent{
1✔
68
                config: cfg,
1✔
69
        }
1✔
70
}
71

72
// Name returns the agent identifier.
73
func (a *Agent) Name() string {
1✔
74
        return AgentName
1✔
75
}
1✔
76

77
// Available checks if the Codex CLI is installed.
78
func (a *Agent) Available() error {
1✔
79
        binary := a.config.Command[0]
1✔
80
        path, err := exec.LookPath(binary)
1✔
81
        if err != nil {
2✔
82
                return fmt.Errorf("codex CLI not found: %w", err)
1✔
83
        }
1✔
84

85
        // Verify it runs
86
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
1✔
87
        defer cancel()
1✔
88

1✔
89
        cmd := exec.CommandContext(ctx, path, "--version")
1✔
90
        if err := cmd.Run(); err != nil {
2✔
91
                return fmt.Errorf("codex CLI not working: %w", err)
1✔
92
        }
1✔
93

94
        return nil
×
95
}
96

97
// Connect establishes connection. Tries WebSocket first, falls back to CLI.
98
func (a *Agent) Connect(ctx context.Context) error {
×
99
        a.mu.Lock()
×
100
        defer a.mu.Unlock()
×
101

×
102
        // Already connected?
×
103
        if a.ws != nil && a.ws.Connected() {
×
104
                return nil
×
105
        }
×
106
        if a.cli != nil && a.cli.Connected() {
×
107
                return nil
×
108
        }
×
109

110
        // Try WebSocket first if preferred
111
        if a.config.PreferWebSocket {
×
112
                ws := NewWebSocketConnection(a.config)
×
113
                if err := ws.Connect(ctx); err == nil {
×
114
                        a.ws = ws
×
115
                        a.mode = agent.ModeWebSocket
×
116

×
117
                        return nil
×
118
                }
×
119
                // WebSocket failed, try CLI
120
        }
121

122
        // Fallback to CLI
123
        cli := NewCLIConnection(a.config)
×
124
        if err := cli.Connect(ctx); err != nil {
×
125
                return fmt.Errorf("connect failed (both WebSocket and CLI): %w", err)
×
126
        }
×
127

128
        a.cli = cli
×
129
        a.mode = agent.ModeCLI
×
130

×
131
        return nil
×
132
}
133

134
// Connected returns true if the agent is connected.
135
func (a *Agent) Connected() bool {
1✔
136
        a.mu.RLock()
1✔
137
        defer a.mu.RUnlock()
1✔
138

1✔
139
        if a.ws != nil {
1✔
140
                return a.ws.Connected()
×
141
        }
×
142
        if a.cli != nil {
1✔
143
                return a.cli.Connected()
×
144
        }
×
145

146
        return false
1✔
147
}
148

149
// Mode returns the current connection mode.
150
func (a *Agent) Mode() agent.ConnectionMode {
1✔
151
        a.mu.RLock()
1✔
152
        defer a.mu.RUnlock()
1✔
153

1✔
154
        return a.mode
1✔
155
}
1✔
156

157
// SendPrompt sends a prompt and returns streaming events.
158
func (a *Agent) SendPrompt(ctx context.Context, prompt string) (<-chan agent.Event, error) {
1✔
159
        a.mu.RLock()
1✔
160
        defer a.mu.RUnlock()
1✔
161

1✔
162
        switch a.mode {
1✔
163
        case agent.ModeWebSocket:
×
164
                if a.ws == nil {
×
165
                        return nil, errors.New("websocket not connected")
×
166
                }
×
167

168
                return a.ws.SendPrompt(ctx, prompt)
×
169
        case agent.ModeCLI:
×
170
                if a.cli == nil {
×
171
                        return nil, errors.New("cli not connected")
×
172
                }
×
173

174
                return a.cli.SendPrompt(ctx, prompt)
×
175
        case agent.ModeAPI:
×
176
                return nil, errors.New("ModeAPI not yet implemented")
×
177
        default:
1✔
178
                return nil, errors.New("not connected")
1✔
179
        }
180
}
181

182
// HandlePermission responds to a permission request.
183
func (a *Agent) HandlePermission(requestID string, approved bool) error {
1✔
184
        a.mu.RLock()
1✔
185
        defer a.mu.RUnlock()
1✔
186

1✔
187
        if a.mode == agent.ModeWebSocket && a.ws != nil {
1✔
188
                return a.ws.HandlePermission(requestID, approved)
×
189
        }
×
190
        // CLI mode doesn't support interactive permission handling
191
        return nil
1✔
192
}
193

194
// Close closes the connection.
195
func (a *Agent) Close() error {
1✔
196
        a.mu.Lock()
1✔
197
        defer a.mu.Unlock()
1✔
198

1✔
199
        var err error
1✔
200
        if a.ws != nil {
1✔
201
                err = a.ws.Close()
×
202
                a.ws = nil
×
203
        }
×
204
        if a.cli != nil {
1✔
205
                err = a.cli.Close()
×
206
                a.cli = nil
×
207
        }
×
208
        a.mode = ""
1✔
209

1✔
210
        return err
1✔
211
}
212

213
// WithEnv returns a new Agent with an added environment variable.
214
func (a *Agent) WithEnv(key, value string) agent.Agent {
1✔
215
        newCfg := a.config
1✔
216
        if newCfg.Environment == nil {
1✔
217
                newCfg.Environment = make(map[string]string)
×
218
        }
×
219
        // Copy existing env
220
        env := make(map[string]string, len(a.config.Environment)+1)
1✔
221
        for k, v := range a.config.Environment {
2✔
222
                env[k] = v
1✔
223
        }
1✔
224
        env[key] = value
1✔
225
        newCfg.Environment = env
1✔
226

1✔
227
        return NewWithConfig(newCfg)
1✔
228
}
229

230
// WithArgs returns a new Agent with additional CLI arguments.
231
func (a *Agent) WithArgs(args ...string) agent.Agent {
1✔
232
        newCfg := a.config
1✔
233
        newArgs := make([]string, len(a.config.Args)+len(args))
1✔
234
        copy(newArgs, a.config.Args)
1✔
235
        copy(newArgs[len(a.config.Args):], args)
1✔
236
        newCfg.Args = newArgs
1✔
237

1✔
238
        return NewWithConfig(newCfg)
1✔
239
}
1✔
240

241
// WithWorkDir returns a new Agent with a different working directory.
242
func (a *Agent) WithWorkDir(dir string) agent.Agent {
1✔
243
        newCfg := a.config
1✔
244
        newCfg.WorkDir = dir
1✔
245

1✔
246
        return NewWithConfig(newCfg)
1✔
247
}
1✔
248

249
// WithTimeout returns a new Agent with a different timeout.
250
func (a *Agent) WithTimeout(d time.Duration) agent.Agent {
1✔
251
        newCfg := a.config
1✔
252
        newCfg.Timeout = d
1✔
253

1✔
254
        return NewWithConfig(newCfg)
1✔
255
}
1✔
256

257
// WithModel returns a new Agent with a specific model.
258
func (a *Agent) WithModel(model string) *Agent {
1✔
259
        newCfg := a.config
1✔
260
        newCfg.Model = model
1✔
261

1✔
262
        return NewWithConfig(newCfg)
1✔
263
}
1✔
264

265
// WithPermissionHandler returns a new Agent with a custom permission handler.
266
func (a *Agent) WithPermissionHandler(handler agent.PermissionHandler) *Agent {
1✔
267
        newCfg := a.config
1✔
268
        newCfg.PermissionHandler = handler
1✔
269

1✔
270
        return NewWithConfig(newCfg)
1✔
271
}
1✔
272

273
// Register adds the Codex agent to a registry with default config.
274
func Register(r *agent.Registry) error {
1✔
275
        return r.Register(New())
1✔
276
}
1✔
277

278
// RegisterWithPermissionHandler adds the Codex agent with a custom permission handler.
UNCOV
279
func RegisterWithPermissionHandler(r *agent.Registry, handler agent.PermissionHandler) error {
×
UNCOV
280
        cfg := agent.DefaultConfig()
×
UNCOV
281
        cfg.Command = []string{"codex"}
×
UNCOV
282
        cfg.PermissionHandler = handler
×
UNCOV
283

×
UNCOV
284
        return r.Register(NewWithConfig(Config{Config: cfg}))
×
UNCOV
285
}
×
286

287
// Ensure Agent implements agent.Agent.
288
var _ agent.Agent = (*Agent)(nil)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc