Просмотр исходного кода

feat: add session retry functionality with error handling and UI integration

paviko 1 месяц назад
Родитель
Сommit
c8d1a76453

+ 48 - 0
packages/opencode/src/webgui/server/webgui.ts

@@ -1,6 +1,7 @@
 import { Storage } from "@/storage/storage.ts"
 import { Hono } from "hono"
 import { validator, describeRoute, resolver } from "hono-openapi"
+import { stream } from "hono/streaming"
 import { z } from "zod"
 import * as State from "@/webgui/state/state.ts"
 import { StateSchema } from "@/webgui/state/state.ts"
@@ -9,6 +10,8 @@ import { Auth } from "../../auth"
 import { Instance } from "../../project/instance"
 import { mapValues } from "remeda"
 import { Provider } from "@/provider/provider.ts"
+import { SessionPrompt } from "../../session/prompt"
+import { MessageV2 } from "../../session/message-v2"
 
 const StatePatchSchema = StateSchema.partial()
 
@@ -512,3 +515,48 @@ export const WebGuiRoute = new Hono()
       return c.json(updated)
     },
   )
+  .post(
+    "/session/:sessionID/retry",
+    describeRoute({
+      summary: "Retry session",
+      description: "Retry the assistant loop for a session without sending a new message.",
+      operationId: "session.retry",
+      responses: {
+        200: {
+          description: "Assistant response",
+          content: {
+            "application/json": {
+              schema: resolver(MessageV2.WithParts),
+            },
+          },
+        },
+        ...errors(400, 404),
+      },
+    }),
+    validator(
+      "param",
+      z.object({
+        sessionID: z.string().meta({ description: "Session ID" }),
+      }),
+    ),
+    async (c) => {
+      const { sessionID } = c.req.valid("param")
+      try {
+        c.status(200)
+        c.header("Content-Type", "application/json")
+        return stream(c, async (stream) => {
+          try {
+            const msg = await SessionPrompt.loop(sessionID)
+            stream.write(JSON.stringify(msg))
+          } catch (e) {
+            console.error(`[WebGui] Error in retry loop for session ${sessionID}:`, e)
+            // Error during stream - though we didn't write anything yet
+            stream.write(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }))
+          }
+        })
+      } catch (e) {
+        console.error(`[WebGui] Failed to initiate retry stream for session ${sessionID}:`, e)
+        return c.json({ error: e instanceof Error ? e.message : String(e) }, 500)
+      }
+    },
+  )

+ 61 - 23
packages/opencode/webgui/src/components/MessageList/SessionErrorPart.tsx

@@ -1,3 +1,5 @@
+import { useSession } from "../../state/SessionContext"
+import { useCallback } from "react"
 import type { SessionErrorPart as SessionErrorPartType } from "../../types/messages"
 
 interface SessionErrorPartProps {
@@ -5,35 +7,71 @@ interface SessionErrorPartProps {
 }
 
 export function SessionErrorPart({ part }: SessionErrorPartProps) {
+  const { currentSession, retrySession, isIdle } = useSession()
+
+  const handleRetry = useCallback(() => {
+    if (currentSession?.id) {
+      retrySession(currentSession.id)
+    }
+  }, [currentSession?.id, retrySession])
+
   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 className="px-3 py-2 bg-red-50 dark:bg-red-900/10 flex items-start justify-between gap-2">
+        <div className="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 className="text-sm text-red-700 dark:text-red-300">
-            {part.message}
+          <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>
+
+        {isIdle && (
+          <button
+            onClick={handleRetry}
+            className="shrink-0 flex items-center gap-1.5 px-2 py-1 text-[11px] font-medium bg-white dark:bg-white/5 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 rounded hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors shadow-sm"
+          >
+            <svg
+              xmlns="http://www.w3.org/2000/svg"
+              width="12"
+              height="12"
+              viewBox="0 0 24 24"
+              fill="none"
+              stroke="currentColor"
+              strokeWidth="2"
+              strokeLinecap="round"
+              strokeLinejoin="round"
+            >
+              <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
+              <path d="M3 3v5h5" />
+              <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
+              <path d="M16 16h5v5" />
+            </svg>
+            Retry
+          </button>
+        )}
       </div>
     </div>
   )
 }
+

+ 24 - 0
packages/opencode/webgui/src/lib/api/sdkClient.ts

@@ -49,6 +49,30 @@ interface PathResponse {
  */
 export const sdk = {
   ...baseClient,
+  session: Object.assign(baseClient.session, {
+    retry: async (options: { path: { sessionID: string } }) => {
+      try {
+        const response = await fetch(`/app/api/session/${options.path.sessionID}/retry`, {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+        })
+
+        if (!response.ok) {
+          return { error: { message: "Failed to retry session" }, data: null }
+        }
+
+        const data = await response.json()
+        return { data, error: null }
+      } catch (error) {
+        return {
+          error: { message: error instanceof Error ? error.message : "Unknown error" },
+          data: null,
+        }
+      }
+    },
+  }) as (typeof baseClient.session) & {
+    retry: (options: { path: { sessionID: string } }) => Promise<any>
+  },
   config: {
     get: baseClient.config.get.bind(baseClient.config),
     update: baseClient.config.update.bind(baseClient.config),

+ 18 - 0
packages/opencode/webgui/src/state/SessionContext.tsx

@@ -60,6 +60,7 @@ interface SessionContextState {
   revertToMessage: (sessionId: string, messageId: string, partId?: string) => Promise<Session | null>
   unrevertSession: (sessionId: string) => Promise<Session | null>
   redoNext: (sessionId: string) => Promise<Session | null>
+  retrySession: (sessionId: string) => Promise<void>
   clearError: () => void
 }
 
@@ -752,6 +753,22 @@ export function SessionProvider({ children }: SessionProviderProps) {
     [currentSession, revertToMessage, unrevertSession],
   )
 
+  /**
+   * Retry a session's execution
+   */
+  const retrySession = useCallback(async (sessionId: string) => {
+    console.log("[SessionContext] Retrying session:", sessionId)
+    setIsIdle(false)
+    try {
+      await sdk.session.retry({ path: { sessionID: sessionId } })
+    } catch (err) {
+      const errorMsg = err instanceof Error ? err.message : "Failed to retry session"
+      console.error("[SessionContext] Failed to retry session:", errorMsg)
+      setError(new Error(errorMsg))
+      setIsIdle(true)
+    }
+  }, [])
+
   /**
    * Clear the current error
    */
@@ -871,6 +888,7 @@ export function SessionProvider({ children }: SessionProviderProps) {
     revertToMessage,
     unrevertSession,
     redoNext,
+    retrySession,
     clearError,
   }