forked from bots-garden/ori
| ✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS | 1 | package bridge |
| 2 | ||
| 3 | import ( | |
| 4 | "context" | |
| 5 | "net/http" | |
| 6 | ||
| 7 | "github.com/coder/websocket" | |
| 8 | ) | |
| 9 | ||
| 10 | // WebSocketHandler returns the HTTP handler that upgrades a request to a | |
| 11 | // WebSocket and attaches it to the bridge: replayed history first, then live | |
| 12 | // events, while incoming messages feed HandleIncoming. | |
| 13 | // | |
| 14 | // Example: | |
| 15 | // | |
| 16 | // mux.Handle("GET /ws", b.WebSocketHandler()) | |
| 17 | func (b *Bridge) WebSocketHandler() http.Handler { | |
| 18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 19 | conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{ | |
| 20 | // ori serves localhost setups (possibly port-forwarded); the | |
| 21 | // SPA and this endpoint share an origin in production, and the | |
| 22 | // Vite dev server proxies /ws, so localhost patterns suffice. | |
| 23 | OriginPatterns: []string{"localhost:*", "127.0.0.1:*"}, | |
| 24 | }) | |
| 25 | if err != nil { | |
| 26 | b.logger.Warn("websocket accept failed", "error", err) | |
| 27 | return | |
| 28 | } | |
| 29 | defer func() { _ = conn.CloseNow() }() | |
| 30 | b.serveClient(r.Context(), conn) | |
| 31 | }) | |
| 32 | } | |
| 33 | ||
| 34 | // serveClient runs one client's session: replay, then a write pump for live | |
| 35 | // events alongside a read loop for incoming messages, until either side ends. | |
| 36 | func (b *Bridge) serveClient(ctx context.Context, conn *websocket.Conn) { | |
| 37 | events, replay, unsubscribe := b.Subscribe() | |
| 38 | defer unsubscribe() | |
| 39 | ||
| 40 | if !writeEvents(ctx, conn, replay) { | |
| 41 | return | |
| 42 | } | |
| 43 | ||
| 44 | writeDone := make(chan struct{}) | |
| 45 | go func() { | |
| 46 | defer close(writeDone) | |
| 47 | pumpEvents(ctx, conn, events) | |
| 48 | }() | |
| 49 | ||
| 50 | b.readLoop(ctx, conn) | |
| 51 | ||
| 52 | // Stop the write pump by closing its channel, then close cleanly. | |
| 53 | unsubscribe() | |
| 54 | <-writeDone | |
| 55 | _ = conn.Close(websocket.StatusNormalClosure, "") | |
| 56 | } | |
| 57 | ||
| 58 | // writeEvents sends a batch of events; false means the connection is gone. | |
| 59 | func writeEvents(ctx context.Context, conn *websocket.Conn, events [][]byte) bool { | |
| 60 | for _, event := range events { | |
| 61 | if err := conn.Write(ctx, websocket.MessageText, event); err != nil { | |
| 62 | return false | |
| 63 | } | |
| 64 | } | |
| 65 | return true | |
| 66 | } | |
| 67 | ||
| 68 | // pumpEvents forwards live bridge events to the socket until the | |
| 69 | // subscription channel closes or a write fails. | |
| 70 | func pumpEvents(ctx context.Context, conn *websocket.Conn, events <-chan []byte) { | |
| 71 | for event := range events { | |
| 72 | if err := conn.Write(ctx, websocket.MessageText, event); err != nil { | |
| 73 | return | |
| 74 | } | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | // readLoop feeds client messages to the bridge until the socket closes. | |
| 79 | func (b *Bridge) readLoop(ctx context.Context, conn *websocket.Conn) { | |
| 80 | for { | |
| 81 | kind, data, err := conn.Read(ctx) | |
| 82 | if err != nil { | |
| 83 | return | |
| 84 | } | |
| 85 | if kind == websocket.MessageText { | |
| 86 | b.HandleIncoming(ctx, data) | |
| 87 | } | |
| 88 | } | |
| 89 | } |