Explorar o código

feat: implement session error handling in MessagesContext and add SessionErrorPart component

paviko hai 3 meses
pai
achega
c75c5f1621

+ 1 - 25
packages/opencode/webgui/src/App.tsx

@@ -268,31 +268,7 @@ function AppContent() {
         }
       }
 
-      // Handle session errors
-      if (event.type === "session.error") {
-        const { sessionID, error } = event.properties as { sessionID: string; error: unknown }
-        if (currentSession?.id === sessionID) {
-          const message = (() => {
-            if (!error) return "An error occurred in the session"
-            if (typeof error === "string") return error
-            if (typeof error === "object") {
-              const data = (error as { data?: { message?: unknown }; message?: unknown }).data
-              const dataMessage = data && typeof data.message === "string" ? data.message : undefined
-              if (dataMessage) return dataMessage
-              const topMessage = (error as { message?: unknown }).message
-              if (typeof topMessage === "string" && topMessage.length > 0) return topMessage
-            }
-            return "An error occurred in the session"
-          })()
-
-          console.error("[App] Session error:", message)
-          showToast(message, {
-            title: "Session Error",
-            variant: "error",
-            duration: 8000,
-          })
-        }
-      }
+      // session.error is handled in MessagesContext.tsx to show a persistent message
 
       // Handle session compaction
       if (event.type === "session.compacted") {

+ 9 - 3
packages/opencode/webgui/src/components/MessageList/MessagePart.tsx

@@ -1,15 +1,16 @@
-import type { Part } from "../../state/MessagesContext"
+import type { Part, WebguiPart } from "../../state/MessagesContext"
 import { TextPart } from "./TextPart"
 import { ReasoningPart } from "./ReasoningPart"
 import { ToolPart } from "../parts/ToolPart"
 import { PatchPart } from "../parts/PatchPart"
 import { SnapshotPart } from "../parts/SnapshotPart"
 import { RetryPart } from "../parts/RetryPart"
+import { SessionErrorPart } from "./SessionErrorPart"
 
 interface MessagePartProps {
-  part: Part
+  part: WebguiPart
   isUser: boolean
-  allParts: Part[]
+  allParts: WebguiPart[]
   durationMs?: number
   sessionID?: string
   messageID?: string
@@ -153,5 +154,10 @@ export function MessagePart({
     return <RetryPart key={part.id} part={part as any} />
   }
 
+  // Session errors
+  if (part.type === "session-error") {
+    return <SessionErrorPart key={part.id} part={part} />
+  }
+
   return null
 }

+ 39 - 0
packages/opencode/webgui/src/components/MessageList/SessionErrorPart.tsx

@@ -0,0 +1,39 @@
+import type { SessionErrorPart as SessionErrorPartType } from "../../types/messages"
+
+interface SessionErrorPartProps {
+  part: SessionErrorPartType
+}
+
+export function SessionErrorPart({ part }: SessionErrorPartProps) {
+  return (
+    <div className="modern-card overflow-hidden border-red-200 dark:border-red-900/30">
+      <div className="px-3 py-2 bg-red-50 dark:bg-red-900/10 flex items-start gap-2">
+        <div className="mt-0.5 text-red-600 dark:text-red-400">
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="14"
+            height="14"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            strokeWidth="2"
+            strokeLinecap="round"
+            strokeLinejoin="round"
+          >
+            <circle cx="12" cy="12" r="10" />
+            <line x1="12" y1="8" x2="12" y2="12" />
+            <line x1="12" y1="16" x2="12.01" y2="16" />
+          </svg>
+        </div>
+        <div>
+          <div className="text-[10px] uppercase font-bold tracking-wider text-red-600 dark:text-red-400 mb-0.5">
+            Session Error
+          </div>
+          <div className="text-sm text-red-700 dark:text-red-300">
+            {part.message}
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 5 - 5
packages/opencode/webgui/src/lib/messagesStore.ts

@@ -3,7 +3,7 @@
  * Eliminates duplicate array cloning logic and type casting from MessagesProvider
  */
 
-import type { Message, Part, SDKMessage, TextPart } from "../types/messages"
+import type { Message, WebguiPart, SDKMessage, TextPart } from "../types/messages"
 
 /**
  * Upsert a message (add new or update existing)
@@ -59,7 +59,7 @@ export function removeMessage(messages: Message[], messageID: string): Message[]
 /**
  * Upsert a part in a message (add new or update existing)
  */
-export function upsertPart(messages: Message[], messageID: string, part: Part): Message[] {
+export function upsertPart(messages: Message[], messageID: string, part: WebguiPart): Message[] {
   const messageIndex = messages.findIndex((m) => m.info.id === messageID)
 
   if (messageIndex < 0) return messages
@@ -84,7 +84,7 @@ export function upsertPart(messages: Message[], messageID: string, part: Part):
 /**
  * Apply a text delta to a part (for streaming)
  */
-export function applyPartDelta(messages: Message[], messageID: string, part: Part, delta: string): Message[] {
+export function applyPartDelta(messages: Message[], messageID: string, part: WebguiPart, delta: string): Message[] {
   if (part.type !== "text") {
     // For non-text parts, just upsert normally
     return upsertPart(messages, messageID, part)
@@ -121,7 +121,7 @@ export function applyPartDelta(messages: Message[], messageID: string, part: Par
 /**
  * Update a specific part in a message
  */
-export function updatePart(messages: Message[], messageID: string, partID: string, update: Partial<Part>): Message[] {
+export function updatePart(messages: Message[], messageID: string, partID: string, update: Partial<WebguiPart>): Message[] {
   const messageIndex = messages.findIndex((m) => m.info.id === messageID)
 
   if (messageIndex < 0) return messages
@@ -133,7 +133,7 @@ export function updatePart(messages: Message[], messageID: string, partID: strin
   if (partIndex < 0) return messages
 
   const updatedParts = [...message.parts]
-  updatedParts[partIndex] = { ...updatedParts[partIndex], ...update } as Part
+  updatedParts[partIndex] = { ...updatedParts[partIndex], ...update } as WebguiPart
   updated[messageIndex] = { ...message, parts: updatedParts }
 
   return updated

+ 61 - 6
packages/opencode/webgui/src/state/MessagesContext.tsx

@@ -1,6 +1,6 @@
 import { createContext, useContext, useState, useCallback, type ReactNode } from "react"
 import { useEventHandler, type EventEmitter, type ServerEvent } from "../lib/api/events"
-import type { Message, Part, SDKMessage } from "../types/messages"
+import type { Message, Part, WebguiPart, SDKMessage } from "../types/messages"
 // PermissionRequest type based on new permission system (permission.asked event)
 interface PermissionRequest {
   id: string
@@ -20,15 +20,16 @@ import { useSession } from "./SessionContext"
 import { reloadPath } from "../lib/ideBridge"
 
 // Re-export types for convenience
-export type { Message, Part, SDKMessage } from "../types/messages"
+export type { Message, Part, WebguiPart, SDKMessage } from "../types/messages"
 
 interface MessagesContextValue {
   messages: Message[]
   addMessage: (message: Message) => void
+  addSessionError: (sessionID: string, error: unknown) => void
   updateMessage: (messageID: string, update: Partial<Message>) => void
   removeMessage: (messageID: string) => void
-  addPart: (messageID: string, part: Part) => void
-  updatePart: (messageID: string, partID: string, update: Partial<Part>) => void
+  addPart: (messageID: string, part: WebguiPart) => void
+  updatePart: (messageID: string, partID: string, update: Partial<WebguiPart>) => void
   removePart: (messageID: string, partID: string) => void
   clearMessages: () => void
   getMessagesBySession: (sessionID: string) => Message[]
@@ -61,6 +62,46 @@ export function MessagesProvider({ children, emitter }: MessagesProviderProps) {
     setMessages((prev) => Store.upsertMessage(prev, message))
   }, [])
 
+  // Add an error message for a session
+  const addSessionError = useCallback((sessionID: string, error: unknown) => {
+    const text = (() => {
+      if (!error) return "An error occurred in the session"
+      if (typeof error === "string") return error
+      if (typeof error === "object") {
+        const data = (error as { data?: { message?: unknown }; message?: unknown }).data
+        const dataMessage = data && typeof data.message === "string" ? data.message : undefined
+        if (dataMessage) return dataMessage
+        const topMessage = (error as { message?: unknown }).message
+        if (typeof topMessage === "string" && topMessage.length > 0) return topMessage
+      }
+      return "An error occurred in the session"
+    })()
+
+    const errorID = `error-${Date.now()}`
+    const errorMessage: Message = {
+      info: ({
+        id: errorID,
+        sessionID,
+        role: "assistant",
+        time: {
+          created: Date.now(),
+          updated: Date.now(),
+        },
+      } as unknown) as SDKMessage,
+      parts: [
+        {
+          id: `part-${errorID}`,
+          type: "session-error",
+          sessionID,
+          messageID: errorID,
+          message: text,
+        } as WebguiPart,
+      ],
+    }
+
+    setMessages((prev) => Store.upsertMessage(prev, errorMessage))
+  }, [])
+
   // Update a message
   const updateMessage = useCallback((messageID: string, update: Partial<Message>) => {
     setMessages((prev) => Store.updateMessage(prev, messageID, update))
@@ -72,12 +113,12 @@ export function MessagesProvider({ children, emitter }: MessagesProviderProps) {
   }, [])
 
   // Add a part to a message
-  const addPart = useCallback((messageID: string, part: Part) => {
+  const addPart = useCallback((messageID: string, part: WebguiPart) => {
     setMessages((prev) => Store.upsertPart(prev, messageID, part))
   }, [])
 
   // Update a specific part in a message
-  const updatePart = useCallback((messageID: string, partID: string, update: Partial<Part>) => {
+  const updatePart = useCallback((messageID: string, partID: string, update: Partial<WebguiPart>) => {
     setMessages((prev) => Store.updatePart(prev, messageID, partID, update))
   }, [])
 
@@ -151,6 +192,18 @@ export function MessagesProvider({ children, emitter }: MessagesProviderProps) {
     [addPart, setReasoning],
   )
 
+  // Listen to session.error events
+  const handleSessionError = useCallback(
+    (event: ServerEvent) => {
+      if (event.type === "session.error") {
+        const { sessionID, error } = event.properties as { sessionID: string; error: unknown }
+        console.error("[MessagesContext] Session error:", sessionID, error)
+        addSessionError(sessionID, error)
+      }
+    },
+    [addSessionError],
+  )
+
   // Listen to message.removed events
   const handleMessageRemoved = useCallback(
     (event: ServerEvent) => {
@@ -277,6 +330,7 @@ export function MessagesProvider({ children, emitter }: MessagesProviderProps) {
   // Subscribe to events if emitter is provided
   useEventHandler(emitter ?? null, "message.updated", handleMessageUpdated)
   useEventHandler(emitter ?? null, "message.part.updated", handlePartUpdated)
+  useEventHandler(emitter ?? null, "session.error", handleSessionError)
   useEventHandler(emitter ?? null, "message.removed", handleMessageRemoved)
   useEventHandler(emitter ?? null, "message.part.removed", handlePartRemoved)
   useEventHandler(emitter ?? null, "permission.asked", handlePermissionAsked)
@@ -285,6 +339,7 @@ export function MessagesProvider({ children, emitter }: MessagesProviderProps) {
   const value: MessagesContextValue = {
     messages,
     addMessage,
+    addSessionError,
     updateMessage,
     removeMessage,
     addPart,

+ 11 - 1
packages/opencode/webgui/src/types/messages.ts

@@ -38,11 +38,21 @@ export type {
   SDKMessage,
 }
 
+export interface SessionErrorPart {
+  type: "session-error"
+  id: string
+  sessionID: string
+  messageID: string
+  message: string
+}
+
+export type WebguiPart = Part | SessionErrorPart
+
 // SDK's Message is the discriminated union (UserMessage | AssistantMessage)
 // Webgui structure wraps this with parts array
 export interface Message {
   info: SDKMessage
-  parts: Part[]
+  parts: WebguiPart[]
 }
 
 // Type guard helpers