changeset 175:71ad34a8bc9a hg-web

[HgWeb] Can stream hg response now. Added react page for hg web since we use json anyway.
author MrJuneJune <me@mrjunejune.com>
date Tue, 20 Jan 2026 06:06:47 -0800
parents 1ba8c1df082c
children fed99fc04e12
files gui_ze/gui_ze.bzl hg-web/BUILD hg-web/main.c hg-web/src/index.html hg-web/src/main.tsx hg-web/src/repo-browser.tsx seobeo/BUILD seobeo/s_http_client.c seobeo/s_web.c seobeo/seobeo.h seobeo/seobeo_internal.h
diffstat 11 files changed, 569 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/gui_ze/gui_ze.bzl	Mon Jan 19 18:59:23 2026 -0800
+++ b/gui_ze/gui_ze.bzl	Tue Jan 20 06:06:47 2026 -0800
@@ -176,11 +176,12 @@
     command = """
       cp -r third_party/bun/** . \
       && cp -r {src_folder}/** .  \
-      && export NODE_PATH=./node_modules && {bun_path} build {input_path} --outfile {output_path}
+      && export NODE_PATH=./node_modules && {bun_path} build {input_path} --outfile {output_path} --target browser
       """.format(
       bun_path = ctx.executable._bun.path,
       src_folder = ctx.attr.src_folder,
-      input_path = ctx.file.src.path.split("/")[-1],
+      # Fix this lol
+      input_path = "/".join(ctx.file.src.path.split("/")[-2:]),
       output_path = out.path,
     ),
     progress_message = "Bundling {} with Bun!\n\n".format(ctx.file.src.path),
--- a/hg-web/BUILD	Mon Jan 19 18:59:23 2026 -0800
+++ b/hg-web/BUILD	Tue Jan 20 06:06:47 2026 -0800
@@ -1,19 +1,46 @@
 load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
-load("//gui_ze:gui_ze.bzl", "move_files_into_dir", "bundle")
+load("//gui_ze:gui_ze.bzl", "move_files_into_dir", "bundle", "bun_build")
+
+filegroup(
+  name = "raw_file",
+  srcs = glob(["src/**"]),
+)
+
+filegroup(
+  name = "src_files",
+  srcs = [":raw_file", ":compiled_ts"],
+)
+
+filegroup(
+  name = "all_ts_files",
+  srcs = glob([
+      "**/*.ts",
+      "**/*.tsx",
+      "**/*.js",
+      "**/*.jsx",
+  ], allow_empty=True)
+)
+
+bun_build(
+  name = "page",
+  src = "src/main.tsx",
+  src_folder = "hg-web",
+  data = [
+    "//third_party/bun:bun_files",
+    ":all_ts_files",
+  ],
+  visibility = ["//visibility:public"],
+)
 
 move_files_into_dir(
   name = "compiled_ts",
   srcs = [
     "//markdown_converter:markdown_to_html",
+    ":page",
   ],
   dest = "src",
 )
 
-filegroup(
-  name = "src_files",
-  srcs = glob(["src/**"]) + [":compiled_ts"],
-)
-
 cc_binary(
   name = "hg_web_server",
   srcs = ["main.c"],
@@ -32,7 +59,7 @@
 cc_binary(
   name = "hg_web_server_debug",
   srcs = ["main.c"],
-  deps = ["//seobeo:seobeo_tcp_server_ws_debug"],
+  deps = ["//seobeo:seobeo"],
   data = [":src_files"],
 )
 
--- a/hg-web/main.c	Mon Jan 19 18:59:23 2026 -0800
+++ b/hg-web/main.c	Tue Jan 20 06:06:47 2026 -0800
@@ -30,16 +30,16 @@
 
   for (size_t i = 0; i < len; i++)
   {
-  if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) {
-    if (i + 1 < len && input_path[i+1] == '.') {
-    // Skip ".."
-    i++;
-    continue;
+    if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) {
+      if (i + 1 < len && input_path[i+1] == '.') {
+      // Skip ".."
+      i++;
+      continue;
+      }
+      // Skip "."
+      continue;
     }
-    // Skip "."
-    continue;
-  }
-  result[j++] = input_path[i];
+    result[j++] = input_path[i];
   }
   result[j] = '\0';
 
@@ -55,18 +55,27 @@
 Seobeo_Client_Response  *hg_proxy_request(
   const char *method,
   const char *path,
-  const char *req_body)
+  const char *req_body,
+  const char *hg_custom)
 {
   char full_path[MAX_PATH];
   snprintf(full_path, MAX_PATH, "http://%s:%s%s", HG_SERVE_HOST, HG_SERVE_PORT, path);
+  Seobeo_Log(SEOBEO_DEBUG, "HG Proxy PATH %s\n", full_path);
   Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(full_path);
-  Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0 (Array Mode)");
+  Seobeo_Client_Request_Set_Method(p_req, method);
+  Seobeo_Client_Request_Add_Header_Array(p_req, "User-Agent: Seobeo/1.0");
   Seobeo_Client_Request_Add_Header_Array(p_req, "Accept: application/json");
-  Seobeo_Client_Request_Add_Header_Array(p_req, "X-Test-Header: TestValue");
-  if (strcmp(method, "POST"))
-    Seobeo_Client_Request_Set_Method(p_req, "POST");
 
-  Seobeo_Client_Request_Set_Body(p_req, req_body, 0);
+  if (hg_custom && hg_custom[0] != '\0')
+  {
+    char buffer[1024];
+    snprintf(buffer, 1024, "x-hgarg-1: %s", hg_custom);
+    Seobeo_Client_Request_Add_Header_Array(p_req, buffer);
+    Seobeo_Log(SEOBEO_DEBUG, "HG CUSTOM %s\n", buffer);
+  }
+
+  if (req_body)
+    Seobeo_Client_Request_Set_Body(p_req, req_body, strlen(req_body));
   Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
   Seobeo_Client_Request_Destroy(p_req);
   return p_resp;
@@ -88,17 +97,14 @@
 
   char hg_path[MAX_PATH];
   if (strlen(safe_path) > 0)
-  snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path);
+    snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path);
   else
-  snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json");
+    snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json");
 
-  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL);
+  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL);
 
   Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);
 
-  char status[4];
-  snprintf(status, 3, "%i", hg_response->status_code);
-
   if (hg_response->status_code != 200)
   {
     Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n");
@@ -108,10 +114,14 @@
     return resp;
   }
 
+  char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length);
+  char *temp2 = Dowa_Arena_Allocate(arena, 256);
+  snprintf(temp2, 256, "%zu", hg_response->body_length);
+
   Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
   Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
-  Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
-  Seobeo_Client_Response_Destroy(hg_response);
+  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
   return resp;
 }
 
@@ -137,7 +147,7 @@
 
   char hg_path[MAX_PATH];
   snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path);
-  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL);
+  Seobeo_Client_Response  *hg_response = hg_proxy_request("GET", hg_path, NULL, NULL);
 
   Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%i body_len=%zu\n", hg_response->status_code, hg_response->body_length);
 
@@ -160,11 +170,16 @@
     Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
     return resp;
   }
-  
+
+
+  char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length);
+  char *temp2 = Dowa_Arena_Allocate(arena, 256);
+  snprintf(temp2, 256, "%zu", hg_response->body_length);
+
   Dowa_HashMap_Push_Arena(resp, "status", status, arena);
   Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-  Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
-  Seobeo_Client_Response_Destroy(hg_response);
+  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
 
   return resp;
 }
@@ -173,6 +188,117 @@
   return ApiGetFile(req, arena);
 }
 
+// Streaming handler for hg wire protocol - pipes data directly without buffering
+void StreamHgWireProtocol(Seobeo_Handle *p_client, Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method");
+  const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET";
+
+  void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString");
+  const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : "";
+
+  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
+  const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : "";
+
+  const char *hg_custom = req[7].value;
+
+  Seobeo_Log(SEOBEO_DEBUG, "HG Stream Proxy: method=%s query=%s\n", method, query_string);
+
+  // THINKING: Connect to hg serve
+  // This kinda blows, but not a good way to handle it since my client API assumes it is all stored in
+  // buffer and what not.
+  Seobeo_Handle *p_upstream = Seobeo_Stream_Handle_Client_Create(HG_SERVE_HOST, HG_SERVE_PORT, FALSE);
+  if (!p_upstream || p_upstream->socket < 0)
+  {
+    const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 26\r\n\r\nFailed to connect upstream";
+    Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp));
+    Seobeo_Handle_Flush(p_client);
+    if (p_upstream)
+      Seobeo_Handle_Destroy(p_upstream);
+    return;
+  }
+
+  // Create headers
+  // we only allow x-hgarg-1 and content-length
+  char request_buf[8192];
+  int req_len = snprintf(request_buf, sizeof(request_buf),
+    "%s /?%s HTTP/1.1\r\n"
+    "Host: %s:%s\r\n"
+    "User-Agent: Seobeo/1.0\r\n"
+    "Connection: close\r\n",
+    method, query_string, HG_SERVE_HOST, HG_SERVE_PORT);
+
+  if (hg_custom && hg_custom[0] != '\0')
+    req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "x-hgarg-1: %s\r\n", hg_custom);
+
+  if (req_body && req_body[0] != '\0')
+    req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "Content-Length: %zu\r\n\r\n%s", strlen(req_body), req_body);
+  else
+    req_len += snprintf(request_buf + req_len, sizeof(request_buf) - req_len, "\r\n");
+
+  Seobeo_Handle_Queue(p_upstream, (uint8*)request_buf, req_len);
+  if (Seobeo_Handle_Flush(p_upstream) < 0)
+  {
+    const char *err_resp = "HTTP/1.1 502 Bad Gateway\r\nContent-Length: 21\r\n\r\nUpstream write failed";
+    Seobeo_Handle_Queue(p_client, (uint8*)err_resp, strlen(err_resp));
+    Seobeo_Handle_Flush(p_client);
+    Seobeo_Handle_Destroy(p_upstream);
+    return;
+  }
+
+  // Responses 
+  while (1)
+  {
+    int r = Seobeo_Handle_Read(p_upstream);
+    if (r < 0)
+    {
+      Seobeo_Handle_Destroy(p_upstream);
+      return;
+    }
+    if (p_upstream->read_buffer_len >= 4 &&
+        strstr((char*)p_upstream->read_buffer, "\r\n\r\n") != NULL)
+      break;
+    if (r == 0)
+      continue;
+  }
+
+  // TODO: Maybe make this into a separate function instead of internal function as doing this over and over again blows.
+  char *hdr_end = strstr((char*)p_upstream->read_buffer, "\r\n\r\n");
+  if (!hdr_end)
+  {
+    Seobeo_Handle_Destroy(p_upstream);
+    return;
+  }
+  size_t hdr_len = hdr_end - (char*)p_upstream->read_buffer + 4;
+  Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, hdr_len);
+  Seobeo_Handle_Flush(p_client);
+
+  // All body 
+  size_t body_in_buffer = p_upstream->read_buffer_len - hdr_len;
+  if (body_in_buffer > 0)
+  {
+    Seobeo_Handle_Queue(p_client, p_upstream->read_buffer + hdr_len, body_in_buffer);
+    Seobeo_Handle_Flush(p_client);
+  }
+  Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len);
+  while (1)
+  {
+    int n = Seobeo_Handle_Read(p_upstream);
+    if (n > 0)
+    {
+      Seobeo_Handle_Queue(p_client, p_upstream->read_buffer, p_upstream->read_buffer_len);
+      Seobeo_Handle_Flush(p_client);
+      Seobeo_Handle_Consume(p_upstream, p_upstream->read_buffer_len);
+    }
+    else if (n == -2)
+      break;
+    else if (n < 0)
+      break;
+  }
+
+  Seobeo_Handle_Destroy(p_upstream);
+}
+
 Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
@@ -187,27 +313,32 @@
   const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : "";
   size_t body_len = strlen(req_body);
 
+  const char *hg_custom = req[7].value;
   Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len);
+
   Seobeo_Client_Response *hg_response;
-  if (strlen(query_string) > 0)
-  {
-    char temp_path[MAX_PATH];
-    snprintf(temp_path, MAX_PATH, "?%s", query_string);
-    hg_response = hg_proxy_request(method, query_string, req_body);
-  }
-  else
-    hg_response = hg_proxy_request(method, "", req_body);
+
+  char hg_path[MAX_PATH];
+  snprintf(hg_path, sizeof(hg_path), "/?%s", query_string);
+
+  hg_response = hg_proxy_request(method, hg_path, req_body, hg_custom);
 
   Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", hg_response->body_length);
 
   Seobeo_Request_Entry *kv = Dowa_HashMap_Get_Ptr(hg_response->headers, "Content-Type");
 
-  char status[4];
-  snprintf(status, 3, "%i", hg_response->status_code);
+  char *status = Dowa_Arena_Allocate(arena, 5);
+  snprintf(status, 4, "%i", hg_response->status_code);
+
+  // Use binary-safe copy to handle null bytes in mercurial bundle data
+  char *temp1 = Dowa_Arena_Copy(arena, hg_response->body, hg_response->body_length);
+  char *temp2 = Dowa_Arena_Allocate(arena, 256);
+  snprintf(temp2, 256, "%zu", hg_response->body_length);
 
   Dowa_HashMap_Push_Arena(resp, "status", status, arena);
-  Dowa_HashMap_Push_Arena(resp, "content-type", kv ? kv->value : "", arena);
-  Dowa_HashMap_Push_Arena(resp, "body", hg_response->body, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", kv->value, arena);
+  Dowa_HashMap_Push_Arena(resp, "body", temp1, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-length", temp2, arena);
 
   return resp;
 }
@@ -219,12 +350,13 @@
   Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile);
   Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme);
 
-  Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol);
-  Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol);
+  // Use streaming handler for hg wire protocol... 
+  Seobeo_Router_Register_Stream("GET", "/repo", StreamHgWireProtocol);
+  Seobeo_Router_Register_Stream("POST", "/repo", StreamHgWireProtocol);
 
   printf("Starting on Port 6970...\n");
 
-  int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4);
+  int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 1);
 
   Seobeo_Router_Destroy();
 
--- a/hg-web/src/index.html	Mon Jan 19 18:59:23 2026 -0800
+++ b/hg-web/src/index.html	Tue Jan 20 06:06:47 2026 -0800
@@ -9,14 +9,16 @@
 </head>
 <body>
     <main>
-        <div class="header">
+      <div id="root"></div>
+
+<!--        <div class="header">
             <h1>Zenbu Repository</h1>
             <p class="description">Browse and clone this mercurial repository</p>
         </div>
 
         <div class="clone-info">
             <strong>Clone this repository:</strong><br>
-            <code>hg clone http://zenbu.babocoder.com</code>
+            <code>hg clone http://zenbu.babocoder.com/repo</code>
         </div>
 
         <div class="breadcrumb" id="breadcrumb"></div>
@@ -30,10 +32,10 @@
 
         <div class="empty-state" id="emptyState" style="display: none;">
             <p>No files found in this directory</p>
-        </div>
+        </div> -->
     </main>
 
     <script src="/markdown_to_html.js"></script>
-    <script src="/index.js"></script>
+    <script src="/page.js"></script>
 </body>
 </html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/main.tsx	Tue Jan 20 06:06:47 2026 -0800
@@ -0,0 +1,8 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { RepoBrowser } from "./repo-browser";
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+
+// Use JSX syntax (<RepoBrowser />) 
+root.render(<RepoBrowser />);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hg-web/src/repo-browser.tsx	Tue Jan 20 06:06:47 2026 -0800
@@ -0,0 +1,257 @@
+import React, { useState, useEffect } from 'react';
+
+const API_BASE = '/api/repo';
+
+/**
+ * Component: Breadcrumb
+ * Renders the navigation path at the top
+ */
+function Breadcrumb({ currentPath, onNavigate }) {
+  if (!currentPath) {
+    return (
+      <nav id="breadcrumb">
+        <span className="nav-item active">Root</span>
+      </nav>
+    );
+  }
+
+  const parts = currentPath.split('/').filter(p => p);
+  
+  // Create cumulative paths for links
+  // e.g., src/components -> ['src', 'src/components']
+  const crumbs = parts.map((part, index) => ({
+    name: part,
+    fullPath: parts.slice(0, index + 1).join('/')
+  }));
+
+  return (
+    <nav id="breadcrumb">
+      <a 
+        href="/" 
+        onClick={(e) => { e.preventDefault(); onNavigate(''); }}
+      >
+        Root
+      </a>
+      {crumbs.map((crumb, index) => {
+        const isLast = index === crumbs.length - 1;
+        return (
+          <React.Fragment key={crumb.fullPath}>
+            <span className="separator"> / </span>
+            {isLast ? (
+              <span className="nav-item active">{crumb.name}</span>
+            ) : (
+              <a 
+                href={`?path=${encodeURIComponent(crumb.fullPath)}`}
+                onClick={(e) => { e.preventDefault(); onNavigate(crumb.fullPath); }}
+              >
+                {crumb.name}
+              </a>
+            )}
+          </React.Fragment>
+        );
+      })}
+    </nav>
+  );
+}
+
+/**
+ * Component: FileList
+ * Renders the table of directories and files
+ */
+function FileList({ directories, files, onNavigate }) {
+  const isEmpty = directories.length === 0 && files.length === 0;
+
+  if (isEmpty) {
+    return <div className="empty-state">No files found.</div>;
+  }
+
+  return (
+    <div id="fileList">
+      {/* Render Directories */}
+      {directories.map((dir) => (
+        <FileRow 
+          key={dir.abspath}
+          item={dir}
+          icon="📁"
+          isDir={true}
+          onNavigate={onNavigate}
+        />
+      ))}
+
+      {/* Render Files */}
+      {files.map((file) => (
+        <FileRow 
+          key={file.abspath}
+          item={file}
+          icon="📄"
+          isDir={false}
+        />
+      ))}
+    </div>
+  );
+}
+
+/**
+ * Component: FileRow
+ * Individual item row
+ */
+function FileRow({ item, icon, isDir, onNavigate }) {
+  const handleClick = (e) => {
+    if (isDir) {
+      e.preventDefault();
+      onNavigate(item.abspath);
+    }
+    // Files let the default <a> behavior happen (download/open in new tab)
+  };
+
+  // Files link to the raw content API, Dirs link to the app view
+  const href = isDir 
+    ? `?path=${encodeURIComponent(item.abspath)}`
+    : `/api/repo/file?path=${encodeURIComponent(item.abspath)}`;
+  
+  const target = isDir ? undefined : "_blank";
+
+  return (
+    <div className={`file-item ${item.type}`}>
+      <span className="icon">{icon}</span>
+      <span className="name">
+        <a href={href} onClick={handleClick} target={target} rel="noreferrer">
+          {item.basename}
+        </a>
+      </span>
+    </div>
+  );
+}
+
+/**
+ * Component: ReadmeViewer
+ * Renders the README content
+ */
+function ReadmeViewer({ content }) {
+  if (!content) return null;
+
+  return (
+    <div id="readmeSection" style={{ marginTop: '20px', borderTop: '1px solid #eee' }}>
+      <h3>README.md</h3>
+      <div id="readmeContent">
+        <pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
+          {content}
+        </pre>
+      </div>
+    </div>
+  );
+}
+
+
+
+/**
+ * Main Application Component
+ */
+function RepoBrowser() {
+  // State management for path, data, and UI states
+  const [currentPath, setCurrentPath] = useState(getCurrentPath());
+  const [content, setContent] = useState({ files: [], directories: [] });
+  const [readme, setReadme] = useState(null);
+  const [error, setError] = useState(null);
+  const [loading, setLoading] = useState(false);
+
+  // Helper to get path from URL query params
+  function getCurrentPath() {
+    const params = new URLSearchParams(window.location.search);
+    return params.get('path') || '';
+  }
+
+  // Effect: Handle Browser Navigation (Back/Forward buttons)
+  useEffect(() => {
+    const handlePopState = () => setCurrentPath(getCurrentPath());
+    window.addEventListener('popstate', handlePopState);
+    return () => window.removeEventListener('popstate', handlePopState);
+  }, []);
+
+  // Effect: Fetch Data whenever currentPath changes
+  useEffect(() => {
+    fetchDirectory(currentPath);
+    fetchReadme(currentPath);
+  }, [currentPath]);
+
+  // Internal navigation handler (avoids full page reload)
+  const navigate = (path) => {
+    const newUrl = path ? `?path=${encodeURIComponent(path)}` : '/';
+    window.history.pushState({ path }, '', newUrl);
+    setCurrentPath(path);
+  };
+
+  const fetchDirectory = async (path) => {
+    setLoading(true);
+    setError(null);
+    try {
+      const url = path 
+        ? `${API_BASE}/list?path=${encodeURIComponent(path)}` 
+        : `${API_BASE}/list`;
+      
+      const response = await fetch(url);
+      const data = await response.json();
+
+      if (data.error) throw new Error(data.error);
+      
+      // Ensure we always have arrays even if API returns null
+      setContent({
+        files: data.files || [],
+        directories: data.directories || []
+      });
+    } catch (err) {
+      console.error('Error loading directory:', err);
+      setError(err.message);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchReadme = async (path) => {
+    setReadme(null); // Reset previous readme
+    try {
+      const readmePath = path ? `${path}/README.md` : 'README.md';
+      const response = await fetch(`${API_BASE}/readme?path=${encodeURIComponent(readmePath)}`);
+      
+      if (response.ok) {
+        const text = await response.text();
+        setReadme(text);
+      }
+    } catch (err) {
+      // Silently fail for Readme as it's optional
+    }
+  };
+
+  return (
+    <div className="repo-container">
+      <div class="header">
+        <h1>Zenbu Repository</h1>
+        <p class="description">Browse and clone this mercurial repository</p>
+      </div>
+
+      <div class="clone-info">
+        <strong>Clone this repository:</strong>
+        <p><code>hg clone http://zenbu.babocoder.com/repo</code></p>
+      </div>
+
+      <Breadcrumb currentPath={currentPath} onNavigate={navigate} />
+      
+      {error && <div className="error-message">Error: {error}</div>}
+      
+      {loading ? (
+        <div className="loading">Loading...</div>
+      ) : (
+        <>
+          <FileList 
+            directories={content.directories} 
+            files={content.files} 
+            onNavigate={navigate} 
+          />
+          <ReadmeViewer content={readme} />
+        </>
+      )}
+    </div>
+  );
+}
+
+export { RepoBrowser };
--- a/seobeo/BUILD	Mon Jan 19 18:59:23 2026 -0800
+++ b/seobeo/BUILD	Tue Jan 20 06:06:47 2026 -0800
@@ -308,7 +308,7 @@
     "//dowa:dowa",
     "@openssl//:ssl",
   ],
-  defines = ["SEOBEO_WEBSOCKET_SERVER"],
+  defines = ["SEOBEO_WEBSOCKET_SERVER", "SEOBEO_ENABLE_DEBUG"],
   target_compatible_with = [
     "@platforms//os:osx",
   ],
@@ -334,7 +334,7 @@
     "//dowa:dowa",
     "@openssl//:ssl",
   ],
-  defines = ["SEOBEO_WEBSOCKET_SERVER"],
+  defines = ["SEOBEO_WEBSOCKET_SERVER", "SEOBEO_ENABLE_DEBUG"],
   target_compatible_with = [
     "@platforms//os:linux",
   ],
--- a/seobeo/s_http_client.c	Mon Jan 19 18:59:23 2026 -0800
+++ b/seobeo/s_http_client.c	Tue Jan 20 06:06:47 2026 -0800
@@ -69,7 +69,7 @@
 
   memset(p_req, 0, sizeof(Seobeo_Client_Request));
 
-  p_req->p_arena = Dowa_Arena_Create(1024 * 1024);
+  p_req->p_arena = Dowa_Arena_Create(1024 * 1024 * 5);
   if (!p_req->p_arena)
   {
     free(p_req);
@@ -231,7 +231,7 @@
 
   memset(p_resp, 0, sizeof(Seobeo_Client_Response));
 
-  p_resp->p_arena = Dowa_Arena_Create(1024 * 1024 * 5); // 5 MB 
+  p_resp->p_arena = Dowa_Arena_Create(1024 * 1024 * 10); // 10 MB 
   if (!p_resp->p_arena)
   {
     free(p_resp);
@@ -391,7 +391,7 @@
   }
   else
   {
-    size_t cap = 1024 * 1024 * 3;
+    size_t cap = 1024 * 1024 * 5;
     size_t used = 0;
     char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap);
 
@@ -414,7 +414,6 @@
           }
           memcpy(body + used, p_handle->read_buffer, p_handle->read_buffer_len);
           used += p_handle->read_buffer_len;
-          Seobeo_Log(SEOBEO_DEBUG, "Copied %zu bytes, total %zu/%zu\n", used, used + p_handle->read_buffer_len, body_len);
         }
         Seobeo_Handle_Consume(p_handle, (uint32)p_handle->read_buffer_len);
       }
--- a/seobeo/s_web.c	Mon Jan 19 18:59:23 2026 -0800
+++ b/seobeo/s_web.c	Tue Jan 20 06:06:47 2026 -0800
@@ -96,13 +96,14 @@
   // Recording IP to see who is ddosing or any web scrappers...
   void *p_real_ip_kv = Dowa_HashMap_Get_Ptr(p_req_map, "X-Real-IP");
   const char *real_ip = p_real_ip_kv ? ((Seobeo_Request_Entry*)p_real_ip_kv)->value : NULL;
+
   // Fallback
   if (!real_ip)
   {
     void *p_forwarded_kv = Dowa_HashMap_Get_Ptr(p_req_map, "X-Forwarded-For");
     real_ip = p_forwarded_kv ? ((Seobeo_Request_Entry*)p_forwarded_kv)->value : NULL;
   }
-  // Fallback
+
   if (!real_ip)
     real_ip = p_cli_handle->host;
 
@@ -137,7 +138,7 @@
 
   // --- Check for WebSocket upgrade request ---
   #ifdef SEOBEO_WEBSOCKET_SERVER
-  Seobeo_Log(SEOBEO_DEBUG, "Web soceket path \n");
+  Seobeo_Log(SEOBEO_DEBUG, "Web socket path \n");
   if (Seobeo_WebSocket_Server_Handle_Upgrade(p_cli_handle, p_req_map, path))
   {
     Seobeo_Log(SEOBEO_INFO, "WebSocket connection established\n");
@@ -149,7 +150,15 @@
   }
   #endif
 
-  // --- Try to match API route first ---
+  // --- Try to match streaming route first ---
+  Seobeo_Stream_Handler stream_handler = Seobeo_Router_Find_Stream_Handler(method, path, &p_req_map, p_request_arena);
+  if (stream_handler != NULL)
+  {
+    stream_handler(p_cli_handle, p_req_map, p_response_arena);
+    goto clean_up;
+  }
+
+  // --- Try to match API route ---
   Seobeo_Route_Handler handler = Seobeo_Router_Find_Handler(method, path, &p_req_map, p_request_arena);
   if (handler != NULL)
   {
@@ -293,7 +302,10 @@
       break;
 
     if (r == 0)
-      return 1;     // EAGAIN, try again later TODO: Add this as part of Handle struct.
+    {
+      Seobeo_Log(SEOBEO_INFO, "Waiting?\n");
+      continue;     // EAGAIN, try again later TODO: Add this as part of Handle struct.
+    }
   }
 
   // "METHOD SP PATH SP VERSION CRLF"
@@ -408,7 +420,6 @@
     char *next = strstr(line, "\r\n");
     if (!next) break;
 
-    // split at colon
     char *colon = memchr(line, ':', next - line);
     if (colon)
     {
@@ -432,7 +443,6 @@
       memcpy(val, val_start, value_len);
       val[value_len] = '\0';
 
-      // Both key and value are arena-allocated, hashmap will use them
       Dowa_HashMap_Push_Arena(*pp_map, key, val, p_arena);
     }
 
@@ -450,7 +460,6 @@
 
     Seobeo_Log(SEOBEO_DEBUG, "Content-Length=%zu, reading body in chunks...\n", body_len);
 
-    // Allocate buffer for entire body
     char *body = Dowa_Arena_Allocate(p_arena, body_len + 1);
     if (!body)
     {
@@ -567,6 +576,7 @@
   char *method; // "GET", "POST", "PUT", "DELETE"
   char *path_pattern; // "/v1/users/:id/posts/:post_id"
   Seobeo_Route_Handler handler;
+  Seobeo_Stream_Handler stream_handler; // For streaming responses
 
   // Pre-parsed path segments for efficient matching
   char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"]
@@ -588,6 +598,25 @@
   route.method = strdup(method);
   route.path_pattern = strdup(path_pattern);
   route.handler = handler;
+  route.stream_handler = NULL;
+  route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
+  route.segment_count = Dowa_Array_Length(route.path_segments);
+  route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);
+
+  for (size_t i = 0; i < route.segment_count; i++)
+    route.is_param[i] = (route.path_segments[i][0] == ':');
+
+  Dowa_Array_Push(g_routes, route);
+}
+
+void Seobeo_Router_Register_Stream(const char *method, const char *path_pattern, Seobeo_Stream_Handler handler)
+{
+  Seobeo_Route route = {0};
+
+  route.method = strdup(method);
+  route.path_pattern = strdup(path_pattern);
+  route.handler = NULL;
+  route.stream_handler = handler;
   route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
   route.segment_count = Dowa_Array_Length(route.path_segments);
   route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);
@@ -670,6 +699,29 @@
   return NULL;
 }
 
+Seobeo_Stream_Handler Seobeo_Router_Find_Stream_Handler(
+  const char *method,
+  const char *path,
+  Seobeo_Request_Entry **pp_request_map,
+  Dowa_Arena *p_arena)
+{
+  if (g_routes == NULL || method == NULL || path == NULL)
+    return NULL;
+
+  size_t route_count = Dowa_Array_Length(g_routes);
+  for (size_t i = 0; i < route_count; i++)
+  {
+    Seobeo_Route *route = &g_routes[i];
+    if (strcmp(route->method, method) != 0)
+      continue;
+
+    if (route->stream_handler && match_route_and_extract(route, path, pp_request_map, p_arena))
+      return route->stream_handler;
+  }
+
+  return NULL;
+}
+
 void Seobeo_Router_Send_Response(
     Seobeo_Handle *p_handle,
     Seobeo_Request_Entry *p_response_map,
@@ -696,24 +748,23 @@
   const char *body = "";
   void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body");
   if (p_body_kv)
-  {
     body = ((Seobeo_Request_Entry*)p_body_kv)->value;
-  }
 
   const char *content_type = "text/html";
   void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type");
   if (p_content_type_kv)
-  {
     content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value;
-  }
 
-  size_t body_length = strlen(body);
+  // TODO: Update this to be integer
+  size_t body_length;
   void *p_content_length_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-length");
   if (p_content_length_kv)
   {
     const char *content_length_str = ((Seobeo_Request_Entry*)p_content_length_kv)->value;
     body_length = atoi(content_length_str);
   }
+  else
+    body_length = strlen(body);
 
   char *header = Dowa_Arena_Allocate(p_arena, 4096);
   Seobeo_Web_Header_Generate(header, status, content_type, body_length);
@@ -734,6 +785,8 @@
     free(temp);
   }
 
+  printf("hEADER %s\n", header);
+
   Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
   Seobeo_Handle_Queue(p_handle, (uint8_t*)body, body_length); 
   Seobeo_Handle_Flush(p_handle);
--- a/seobeo/seobeo.h	Mon Jan 19 18:59:23 2026 -0800
+++ b/seobeo/seobeo.h	Tue Jan 20 06:06:47 2026 -0800
@@ -325,16 +325,20 @@
 extern void                               Seobeo_WebSocket_Server_Connection_Close(Seobeo_WebSocket_Server_Connection *p_conn, uint16 code, const char *reason);
 
 /* Initialize the router system (called automatically by Seobeo_Web_Server_Start) */
-extern void           Seobeo_Router_Init();
+extern void                  Seobeo_Router_Init();
 /* Register an API route handler. Call before starting server. */
-extern void           Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler);
+extern void                  Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler);
+/* Register a streaming route handler. Handler receives client handle for direct streaming. */
+extern void                  Seobeo_Router_Register_Stream(const char *method, const char *path_pattern, Seobeo_Stream_Handler handler);
 /* Clean up router resources */
-extern void           Seobeo_Router_Destroy();
+extern void                  Seobeo_Router_Destroy();
 /* Find matching route handler (internal use) */
-extern Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method, const char *path, Seobeo_Request_Entry **pp_request_map, Dowa_Arena *p_arena);
+extern Seobeo_Route_Handler  Seobeo_Router_Find_Handler(const char *method, const char *path, Seobeo_Request_Entry **pp_request_map, Dowa_Arena *p_arena);
 /* Send HTTP response from response map (internal use) */
-extern void           Seobeo_Router_Send_Response(Seobeo_Handle *p_handle, Seobeo_Request_Entry *p_response_map, Dowa_Arena *p_arena);
-extern char          *Seobeo_Web_LoadFile(const char *file_path, size_t *p_file_size);
+extern void                  Seobeo_Router_Send_Response(Seobeo_Handle *p_handle, Seobeo_Request_Entry *p_response_map, Dowa_Arena *p_arena);
+extern char                 *Seobeo_Web_LoadFile(const char *file_path, size_t *p_file_size);
+extern Seobeo_Stream_Handler Seobeo_Router_Find_Stream_Handler(const char *method, const char *path, Seobeo_Request_Entry **pp_request_map, Dowa_Arena *p_arena);
+
 
 // --- Helper functions --- //
 /* Destroy handle. It will handle all NULL poointers. */
--- a/seobeo/seobeo_internal.h	Mon Jan 19 18:59:23 2026 -0800
+++ b/seobeo/seobeo_internal.h	Tue Jan 20 06:06:47 2026 -0800
@@ -83,6 +83,13 @@
     Dowa_Arena *p_arena
 );
 
+// Streaming handler - gets direct access to client handle for streaming responses
+typedef void (*Seobeo_Stream_Handler)(
+    Seobeo_Handle *p_client_handle,
+    Seobeo_Request_Entry *p_request_map,
+    Dowa_Arena *p_arena
+);
+
 // --- Parse Header into Dowa Map ---//
 extern int            Seobeo_Web_Header_Parse(Seobeo_Handle *p_handle, Seobeo_Request_Entry **pp_map, Dowa_Arena *p_arena);