diff --git a/cmd/orca/main.go b/cmd/orca/main.go
index 4a70e6c..4bf85cf 100644
--- a/cmd/orca/main.go
+++ b/cmd/orca/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "flag"
"fmt"
"log"
"os"
@@ -8,10 +9,14 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/orca/orca/internal/config"
"github.com/orca/orca/internal/tui"
+ "github.com/orca/orca/internal/web"
"github.com/orca/orca/pkg/kernel"
)
func main() {
+ webMode := flag.Bool("web", false, "Run in web mode on port 8081")
+ flag.Parse()
+
cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
@@ -26,12 +31,20 @@ func main() {
log.Printf("Warning: failed to load skills: %v", err)
}
- m := tui.NewModel(k)
- p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
+ if *webMode {
+ server := web.NewServer(k, 8081)
+ fmt.Println("Starting web server on http://localhost:8081")
+ if err := server.Start(); err != nil {
+ log.Fatalf("Failed to start web server: %v", err)
+ }
+ } else {
+ m := tui.NewModel(k)
+ p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
- if _, err := p.Run(); err != nil {
- fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err)
- os.Exit(1)
+ if _, err := p.Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err)
+ os.Exit(1)
+ }
}
if err := k.Stop(); err != nil {
diff --git a/internal/web/html.go b/internal/web/html.go
new file mode 100644
index 0000000..b42e302
--- /dev/null
+++ b/internal/web/html.go
@@ -0,0 +1,365 @@
+package web
+
+const indexHTML = `
+
+
+
+
+ orca.agent
+
+
+
+
+
+
+
+
Processing...
+
+
+
+
+
+
+
+
Statistics
+
+ Tools:
+ 0
+
+
+ Skills:
+ 0
+
+
+ Agents:
+ 0
+
+
+
+
+
+
+
+
+`
diff --git a/internal/web/server.go b/internal/web/server.go
new file mode 100644
index 0000000..a75343b
--- /dev/null
+++ b/internal/web/server.go
@@ -0,0 +1,203 @@
+package web
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "sync"
+
+ "github.com/orca/orca/pkg/actor"
+ "github.com/orca/orca/pkg/kernel"
+)
+
+type Server struct {
+ kernel *kernel.Kernel
+ port int
+ clients map[string]*SSEClient
+ clientsMu sync.RWMutex
+ msgCounter int
+}
+
+type SSEClient struct {
+ ID string
+ Writer http.ResponseWriter
+ Flusher http.Flusher
+ Done chan bool
+}
+
+type sseWriter struct {
+ client *SSEClient
+ mu sync.Mutex
+}
+
+func (w *sseWriter) Write(p []byte) (n int, err error) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ select {
+ case <-w.client.Done:
+ return len(p), nil
+ default:
+ }
+
+ data := strings.ReplaceAll(string(p), "\n", "")
+ if data == "" {
+ return len(p), nil
+ }
+
+ fmt.Fprintf(w.client.Writer, "data: %s\n\n", data)
+ w.client.Flusher.Flush()
+ return len(p), nil
+}
+
+func NewServer(k *kernel.Kernel, port int) *Server {
+ if port <= 0 {
+ port = 8081
+ }
+ return &Server{
+ kernel: k,
+ port: port,
+ clients: make(map[string]*SSEClient),
+ }
+}
+
+func (s *Server) Start() error {
+ mux := http.NewServeMux()
+
+ mux.HandleFunc("/api/stream", s.handleStream)
+ mux.HandleFunc("/api/chat", s.handleChat)
+ mux.HandleFunc("/api/stats", s.handleStats)
+ mux.HandleFunc("/api/agents", s.handleAgents)
+ mux.HandleFunc("/", s.handleIndex)
+
+ addr := fmt.Sprintf(":%d", s.port)
+ return http.ListenAndServe(addr, mux)
+}
+
+func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+
+ flusher, ok := w.(http.Flusher)
+ if !ok {
+ http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
+ return
+ }
+
+ s.msgCounter++
+ clientID := fmt.Sprintf("client-%d", s.msgCounter)
+ client := &SSEClient{
+ ID: clientID,
+ Writer: w,
+ Flusher: flusher,
+ Done: make(chan bool),
+ }
+
+ s.clientsMu.Lock()
+ s.clients[clientID] = client
+ s.clientsMu.Unlock()
+
+ fmt.Fprintf(w, "event: connected\ndata: %s\n\n", clientID)
+ flusher.Flush()
+
+ <-r.Context().Done()
+
+ s.clientsMu.Lock()
+ delete(s.clients, clientID)
+ s.clientsMu.Unlock()
+ close(client.Done)
+}
+
+func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ var req struct {
+ Message string `json:"message"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ s.clientsMu.RLock()
+ var client *SSEClient
+ for _, c := range s.clients {
+ client = c
+ break
+ }
+ s.clientsMu.RUnlock()
+
+ if client != nil {
+ writer := &sseWriter{client: client}
+ s.kernel.SetStreamWriter(writer)
+ }
+
+ resp, err := s.kernel.SendMessage("user", "llm", req.Message)
+ if err != nil {
+ json.NewEncoder(w).Encode(map[string]string{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ json.NewEncoder(w).Encode(map[string]string{
+ "response": resp,
+ })
+}
+
+func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ stats := map[string]interface{}{
+ "tools": 0,
+ "skills": 0,
+ "agents": 0,
+ }
+
+ if tm := s.kernel.ToolManager(); tm != nil {
+ stats["tools"] = tm.Count()
+ }
+ if sm := s.kernel.SkillManager(); sm != nil {
+ stats["skills"] = len(sm.ListSkills())
+ }
+ if as := s.kernel.ActorSystem(); as != nil {
+ stats["agents"] = as.AgentCount()
+ }
+
+ json.NewEncoder(w).Encode(stats)
+}
+
+func (s *Server) handleAgents(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ var agents []map[string]string
+ if as := s.kernel.ActorSystem(); as != nil {
+ for _, info := range as.AgentInfos() {
+ status := "idle"
+ if info.Status == actor.StatusProcessing {
+ status = "running"
+ }
+ agents = append(agents, map[string]string{
+ "id": info.ID,
+ "status": status,
+ })
+ }
+ }
+
+ json.NewEncoder(w).Encode(agents)
+}
+
+func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Write([]byte(indexHTML))
+}