Add end session user feedback survey with skaband client coordination

Implement comprehensive end session feedback system with thumbs up/down rating,
optional text comments, and proper coordination with skaband clients to ensure
feedback delivery before process termination.

- Add EndFeedback struct with Happy boolean and Comment string fields
- Extend Agent struct with endFeedback field and GetEndFeedback/SetEndFeedback methods
- Update State struct to include End field for exposing feedback in API
- Modify /end handler to accept survey data (happy, comment fields)
- Add skaband client coordination with WaitGroup and wait_for_end parameter

- Replace simple window.confirm with custom modal dialog
- Create comprehensive survey UI with thumbs up/down buttons
- Add optional textarea for detailed feedback
- Implement proper button state management and validation
- Maintain existing redirect behavior after successful end request

- Track clients connecting with /stream?wait_for_end=true parameter
- Use WaitGroup to coordinate end session messaging
- Proper WaitGroup management with conditional defer to prevent double-decrement
- Smart timeout handling: measure elapsed time and ensure minimum 100ms response delay
- Wait up to 2 seconds for waiting clients, timeout gracefully if needed

**Survey Dialog Features:**
- Modal overlay with centered positioning and backdrop click handling
- Visual feedback for thumbs up/down selection with color coding
- Expandable textarea for optional detailed comments
- Cancel option to abort end session process
- Proper event handling and cleanup

**State Management:**
- EndFeedback data flows through Agent -> State -> SSE streams
- Clients waiting for end automatically receive feedback via state updates
- WaitGroup ensures coordinated shutdown after notification delivery
- Exactly one Done() call per Add() to prevent race conditions

**API Contract:**
POST /end now accepts:
{
  "reason": "user requested end of session",
  "happy": true,  // optional boolean
  "comment": "Great experience!"  // optional string
}

**Error Handling:**
- Survey submission proceeds even if rating not selected (null happy value)
- Empty comments are handled gracefully
- Network failures fall back to existing error reporting
- Proper resource cleanup and thread-safe operations

**Testing:**
- Added comprehensive unit tests for EndFeedback functionality
- Updated mockAgent to implement new CodingAgent interface methods
- All existing tests continue to pass
- Manual API testing confirmed survey data flows correctly

This enhances user experience by collecting valuable feedback while maintaining
robust session termination and proper coordination with external monitoring
systems via the skaband protocol.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s16561e134e8e81aak
diff --git a/loop/agent.go b/loop/agent.go
index 438d326..5e28bfc 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -34,6 +34,12 @@
 	userCancelMessage = "user requested agent to stop handling responses"
 )
 
+// EndFeedback represents user feedback when ending a session
+type EndFeedback struct {
+	Happy   bool   `json:"happy"`
+	Comment string `json:"comment"`
+}
+
 type MessageIterator interface {
 	// Next blocks until the next message is available. It may
 	// return nil if the underlying iterator context is done.
@@ -128,6 +134,10 @@
 	CurrentStateName() string
 	// CurrentTodoContent returns the current todo list data as JSON, or empty string if no todos exist
 	CurrentTodoContent() string
+	// GetEndFeedback returns the end session feedback
+	GetEndFeedback() *EndFeedback
+	// SetEndFeedback sets the end session feedback
+	SetEndFeedback(feedback *EndFeedback)
 }
 
 type CodingAgentMessageType string
@@ -378,6 +388,9 @@
 
 	// Port monitoring
 	portMonitor *PortMonitor
+
+	// End session feedback
+	endFeedback *EndFeedback
 }
 
 // NewIterator implements CodingAgent.
@@ -476,6 +489,20 @@
 	return string(content)
 }
 
+// SetEndFeedback sets the end session feedback
+func (a *Agent) SetEndFeedback(feedback *EndFeedback) {
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	a.endFeedback = feedback
+}
+
+// GetEndFeedback gets the end session feedback
+func (a *Agent) GetEndFeedback() *EndFeedback {
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	return a.endFeedback
+}
+
 func (a *Agent) URL() string { return a.url }
 
 // Title returns the current title of the conversation.