Skip to content

Commit 02e1a5b

Browse files
committed
feat: Phase 1 — cancel button, UI overhaul, test fixes
Backend: - Add POST /api/cancel endpoint that cancels the running agent prompt - Use atomic.Value to store/clear the cancel function per prompt - Cancel propagates through the agent loop (LLM HTTP request aborted, context canceled) Frontend: - Cancel button in topbar (appears when busy, red X + pulse animation) - Floating scroll-to-bottom button (appears when scrolled up) - Message fade-in animation (opacity + translateY) - Collapsible long messages (auto-collapse at 500px, Show more/less toggle) - Copy button on message hover (clipboard API, Copied feedback) - Visual differentiation (colored left borders, sender dots, accent tints) - Loading skeleton (pulsing shimmer bars while WebSocket connects) - Mobile responsive (slide-in sidebar with hamburger, overlay, single-column) Tests: - 5 new cancel tests (GET 405, POST 204, idle noop, WS cancel, E2E mock LLM) - New mockLLM helper that auto-handles GET /models discovery call - Fix 3 previously-failing E2E tests (callCount off-by-one from discovery) - All 15 serve tests pass (excluding pre-existing Docker/Learn failures)
1 parent 3d74ec3 commit 02e1a5b

3 files changed

Lines changed: 575 additions & 14 deletions

File tree

cmd/odek/serve.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path/filepath"
1313
"strconv"
1414
"strings"
15+
"sync/atomic"
1516
"time"
1617

1718
"github.com/BackendStack21/kode"
@@ -26,6 +27,10 @@ import (
2627
//go:embed ui/index.html
2728
var uiFS embed.FS
2829

30+
// currentPromptCancel holds the cancel function for the currently executing
31+
// prompt. Used by the POST /api/cancel endpoint to abort a running agent.
32+
var currentPromptCancel atomic.Value
33+
2934
// ── Serve Command ───────────────────────────────────────────────────────
3035

3136
func serveCmd(args []string) error {
@@ -122,6 +127,7 @@ func serveCmd(args []string) error {
122127
mux.HandleFunc("/api/resources", handleResourceSearch(resourceReg))
123128
mux.HandleFunc("/api/sessions", handleSessionList(store))
124129
mux.HandleFunc("/api/sessions/", handleSessionDelete(store))
130+
mux.HandleFunc("/api/cancel", handleCancel)
125131

126132
listener, err := net.Listen("tcp", addr)
127133
if err != nil {
@@ -396,7 +402,16 @@ func handleWS(store *session.Store, resources *resource.Registry, resolved confi
396402
}
397403

398404
// Run prompt — passes the persistent agent for buffer continuity
399-
currentSession = handlePrompt(ctx, conn, store, resources, resolved, agent, currentSession, msg.Content, msg.SessionID, &sessionInputTokens, &sessionOutputTokens)
405+
// Create a cancelable context for this prompt (so POST /api/cancel can abort it)
406+
promptCtx, promptCancel := context.WithCancel(ctx)
407+
currentPromptCancel.Store(promptCancel)
408+
409+
currentSession = handlePrompt(promptCtx, conn, store, resources, resolved, agent, currentSession, msg.Content, msg.SessionID, &sessionInputTokens, &sessionOutputTokens)
410+
411+
// Clear cancel on completion — the prompt is no longer running
412+
// Use a no-op sentinel to avoid atomic.Value panicking on nil store
413+
currentPromptCancel.Store(context.CancelFunc(func() {}))
414+
promptCancel()
400415
}
401416

402417
// WebSocket disconnected — extract episode if enough turns
@@ -687,6 +702,23 @@ func handleSessionDelete(store *session.Store) http.HandlerFunc {
687702
}
688703
}
689704

705+
// handleCancel cancels the currently running prompt, if any.
706+
// POST /api/cancel — cancels the current agent execution.
707+
func handleCancel(w http.ResponseWriter, r *http.Request) {
708+
if r.Method != http.MethodPost {
709+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
710+
return
711+
}
712+
713+
if f := currentPromptCancel.Load(); f != nil {
714+
if cancel, ok := f.(context.CancelFunc); ok {
715+
cancel()
716+
}
717+
}
718+
719+
w.WriteHeader(http.StatusNoContent)
720+
}
721+
690722
// ── Static Handler ─────────────────────────────────────────────────────
691723

692724
func handleStatic() http.HandlerFunc {

0 commit comments

Comments
 (0)