webui/src/data: handle ended and read-only sessions
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 296317b..cc183d3 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -159,6 +159,9 @@
 	DiffLinesRemoved     int                           `json:"diff_lines_removed"`              // Lines removed from sketch-base to HEAD
 	OpenPorts            []Port                        `json:"open_ports,omitempty"`            // Currently open TCP ports
 	TokenContextWindow   int                           `json:"token_context_window,omitempty"`
+	SessionEnded         bool                          `json:"session_ended,omitempty"`
+	CanSendMessages      bool                          `json:"can_send_messages,omitempty"`
+	EndedAt              time.Time                     `json:"ended_at,omitempty"`
 }
 
 // Port represents an open TCP port
diff --git a/webui/src/data.ts b/webui/src/data.ts
index 327ff89..4877ca8 100644
--- a/webui/src/data.ts
+++ b/webui/src/data.ts
@@ -7,7 +7,8 @@
 export type DataManagerEventType =
   | "dataChanged"
   | "connectionStatusChanged"
-  | "initialLoadComplete";
+  | "initialLoadComplete"
+  | "sessionEnded";
 
 /**
  * Connection status types
@@ -46,11 +47,16 @@
     Array<(...args: any[]) => void>
   > = new Map();
 
+  // Session state tracking
+  private isSessionEnded: boolean = false;
+  private userCanSendMessages: boolean = true; // User permission to send messages
+
   constructor() {
     // Initialize empty arrays for each event type
     this.eventListeners.set("dataChanged", []);
     this.eventListeners.set("connectionStatusChanged", []);
     this.eventListeners.set("initialLoadComplete", []);
+    this.eventListeners.set("sessionEnded", []);
 
     // Check connection status periodically
     setInterval(() => this.checkConnectionStatus(), 5000);
@@ -122,6 +128,18 @@
       const state = JSON.parse(event.data) as State;
       this.timelineState = state;
 
+      // Check session state and user permissions from server
+      const stateData = state;
+      if (stateData.session_ended === true) {
+        this.isSessionEnded = true;
+        this.userCanSendMessages = false;
+        console.log("Detected ended session from state event");
+      } else if (stateData.can_send_messages === false) {
+        // Session is active but user has read-only access
+        this.userCanSendMessages = false;
+        console.log("Detected read-only access to active session");
+      }
+
       // Store expected message count for initial load detection
       if (this.expectedMessageCount === null) {
         this.expectedMessageCount = state.message_count;
@@ -140,6 +158,11 @@
         }
       }
 
+      // Update connection status when we receive state
+      if (this.connectionStatus !== "connected" && !this.isSessionEnded) {
+        this.updateConnectionStatus("connected");
+      }
+
       this.checkInitialLoadComplete();
       this.emitEvent("dataChanged", { state, newMessages: [] });
     });
@@ -152,6 +175,40 @@
         this.updateConnectionStatus("connected");
       }
     });
+
+    // Handle session ended events for inactive sessions
+    this.eventSource.addEventListener("session_ended", (event) => {
+      const data = JSON.parse(event.data);
+      console.log("Session ended:", data.message);
+
+      this.isSessionEnded = true;
+      this.userCanSendMessages = false;
+      this.isInitialLoadComplete = true;
+
+      // Close the connection since no more data will come
+      this.closeEventSource();
+
+      // Clear any pending reconnection attempts
+      if (this.reconnectTimer !== null) {
+        window.clearTimeout(this.reconnectTimer);
+        this.reconnectTimer = null;
+      }
+      this.reconnectAttempt = 0;
+
+      // Update status to indicate session has ended
+      this.updateConnectionStatus("disabled", "Session ended");
+
+      // Notify listeners about the state change
+      this.emitEvent("sessionEnded", data);
+      this.emitEvent("dataChanged", {
+        state: this.timelineState,
+        newMessages: [],
+      });
+      this.emitEvent("initialLoadComplete", {
+        messageCount: this.messages.length,
+        expectedCount: this.messages.length,
+      });
+    });
   }
 
   /**
@@ -168,6 +225,12 @@
    * Schedule a reconnection attempt with exponential backoff
    */
   private scheduleReconnect(): void {
+    // Don't schedule reconnections for ended sessions
+    if (this.isSessionEnded) {
+      console.log("Skipping reconnection attempt - session has ended");
+      return;
+    }
+
     if (this.reconnectTimer !== null) {
       window.clearTimeout(this.reconnectTimer);
       this.reconnectTimer = null;
@@ -199,8 +262,8 @@
    * Check heartbeat status to determine if connection is still active
    */
   private checkConnectionStatus(): void {
-    if (this.connectionStatus !== "connected") {
-      return; // Only check if we think we're connected
+    if (this.connectionStatus !== "connected" || this.isSessionEnded) {
+      return; // Only check if we think we're connected and session hasn't ended
     }
 
     const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeatTime;
@@ -492,4 +555,26 @@
       return null;
     }
   }
+
+  /**
+   * Check if this session has ended (no more updates will come)
+   */
+  public get sessionEnded(): boolean {
+    return this.isSessionEnded;
+  }
+
+  /**
+   * Check if the current user can send messages (write access)
+   */
+  public get canSendMessages(): boolean {
+    return this.userCanSendMessages;
+  }
+
+  /**
+   * Check if this is effectively read-only (either ended or no write permission)
+   * @deprecated Use sessionEnded and canSendMessages instead for more precise control
+   */
+  public get readOnlyMode(): boolean {
+    return !this.userCanSendMessages;
+  }
 }
diff --git a/webui/src/types.ts b/webui/src/types.ts
index d4dd475..b333657 100644
--- a/webui/src/types.ts
+++ b/webui/src/types.ts
@@ -103,6 +103,9 @@
 	diff_lines_removed: number;
 	open_ports?: Port[] | null;
 	token_context_window?: number;
+	session_ended?: boolean;
+	can_send_messages?: boolean;
+	ended_at?: string;
 }
 
 export interface TodoItem {