From 403499341fc39777593c2d88ed5b60832e3147a8 Mon Sep 17 00:00:00 2001 From: Rokojori Date: Thu, 16 Jul 2026 15:46:51 +0200 Subject: [PATCH] tunnel: Electron agent, production deployment, streaming, Roject integration, chunk animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit — Electron Tunnel Agent app (tray, login, tunnel list, start/stop/delete/create) — tunnel.rokojori.com deployed (nginx WS upgrade, systemd, certbot, port 3003) — Streaming relay protocol: res_start/res_data/res_end replaces single-shot response — Roject: browse-tunnels button in rojo-settings-panel, /tunnels/browse proxy route — Roject: LLM chat via tunnel (TUNNEL_SERVER_URL, /v1 path, dual-source JWT) — rojo-chat-panel: typeText() splits large relay chunks for smooth streaming appearance — Boards, outline, and history updated Co-Authored-By: Claude Sonnet 4.6 --- .../rojo-chat-panel/rojo-chat-panel.ts | 25 +++- workspace/boards/tasks.html | 13 +-- .../2026/07-July/16-Wednesday/index.html | 109 +++++++++++++++++- workspace/history/index.html | 2 +- workspace/outline/index.html | 32 +++-- 5 files changed, 158 insertions(+), 23 deletions(-) diff --git a/source/components/rojo-chat-panel/rojo-chat-panel.ts b/source/components/rojo-chat-panel/rojo-chat-panel.ts index 93dd59f..f979c44 100644 --- a/source/components/rojo-chat-panel/rojo-chat-panel.ts +++ b/source/components/rojo-chat-panel/rojo-chat-panel.ts @@ -3,6 +3,22 @@ import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/ declare const markdownit: ( options?: Record ) => { render: ( md: string ) => string }; +async function typeText( text: string, onPiece: ( piece: string ) => void ): Promise +{ + const THRESHOLD = 6; + const STEP = 3; + const DELAY_MS = 18; + + if ( text.length <= THRESHOLD ) { onPiece( text ); return; } + + for ( let i = 0; i < text.length; i += STEP ) + { + onPiece( text.slice( i, i + STEP ) ); + if ( i + STEP < text.length ) + await new Promise( r => setTimeout( r, DELAY_MS ) ); + } +} + function extractRawText( node: Node ): string { if ( node.nodeType === Node.TEXT_NODE ) return node.nodeValue ?? ''; @@ -215,9 +231,12 @@ class RojoChatPanel extends HTMLElement const msg = JSON.parse( trimmed ) as { type: string; text?: string }; if ( msg.type === 'CHAT' && msg.text ) { - markdown += msg.text; - assistantBubble.innerHTML = this._md!.render( markdown ); - history.scrollTop = history.scrollHeight; + await typeText( msg.text, piece => + { + markdown += piece; + assistantBubble.innerHTML = this._md!.render( markdown ); + history.scrollTop = history.scrollHeight; + } ); } } catch {} diff --git a/workspace/boards/tasks.html b/workspace/boards/tasks.html index 4dcb7c1..da3abe8 100644 --- a/workspace/boards/tasks.html +++ b/workspace/boards/tasks.html @@ -168,16 +168,15 @@ rokojori-tunnel — Phase 2 - Phase 1 complete: relay server, CRUD tunnel API, WebSocket agent endpoint, - HTTP proxy, test agent script. Tested end-to-end with local LLM (gemma4-coding - on port 8900) — request relayed and response returned correctly. - Service lives at C:\rokojori\projects\web-projects\tunnel. + Production-deployed at tunnel.rokojori.com. Complete so far: + relay server, CRUD API, streaming WebSocket protocol (res_start / res_data / res_end), + HTTP proxy with SSE streaming, Electron Tunnel Agent (tray, login, tunnel list), + Roject browse-tunnels button, Roject LLM chat via tunnel, client-side chunk animation. - Phase 2 remaining: + Remaining: — Allowed users list enforcement (multi-user private access) - — GET /api/tunnels/available with ?purpose= filter — Public access mode (no auth required on proxy route) - — Roject LLM provider picker integrating the discovery API + — GET /api/tunnels/available with ?purpose= filter surfaced in Roject provider picker diff --git a/workspace/history/2026/07-July/16-Wednesday/index.html b/workspace/history/2026/07-July/16-Wednesday/index.html index f4e9557..4f22567 100644 --- a/workspace/history/2026/07-July/16-Wednesday/index.html +++ b/workspace/history/2026/07-July/16-Wednesday/index.html @@ -13,7 +13,7 @@

Wednesday, 16 July 2026

Session History

-

Brainstormed and scaffolded rokojori-tunnel — user-based local tunneling service.

+

Brainstormed and scaffolded rokojori-tunnel; Phase 1 relay built; Electron agent app, production deployment, streaming protocol, Roject integration, and client-side chunk animation added in a second session.

@@ -75,6 +75,87 @@
+
+

Session 2 — Electron agent, deployment, streaming, Roject integration

+ +
+

Electron Tunnel Agent app

+

+ Full Electron desktop app at tunnel/electron-agent/ — system tray icon, + login window (email + password → account.rokojori.com), persistent token storage in + userData/tokens.json, and a main window with a tunnel list. Each tunnel + row shows its status (green dot when agent connected) and Start / Stop / Delete + buttons. A modal handles creating new tunnels. The agent process runs in Electron's + main process via the existing TunnelAgent class (WebSocket, auto-reconnect, + exponential back-off). Build: npm run electron:dev. +

+
+ +
+

Production deployment — tunnel.rokojori.com

+

+ Deployed to the same server as roject.rokojori.com. Key config: +

+
    +
  • deploy/nginx-tunnel.conf — HTTP proxy with WebSocket upgrade for + /api/agent/ (proxy_read_timeout 3600s), TLS via Let's Encrypt
  • +
  • deploy/tunnel-rokojori.service — systemd unit; EnvironmentFile + points to /opt/tunnel-rokojori/.env; PORT=3003 + (3002 was already taken by styles.rokojori.com)
  • +
  • ts-node moved from devDependenciesdependencies + so npm install --omit=dev still installs it on the server
  • +
  • ExecStart uses the local node_modules/.bin/ts-node + to avoid version mismatches with any globally installed npx wrapper
  • +
+
+ +
+

Streaming relay protocol

+

+ Replaced the single-shot RelayResponse (collect all chunks, send one JSON blob) + with a three-message streaming protocol over the agent WebSocket: + res_start (status + headers), res_data (base64 chunk), + res_end. The proxy route calls res.flushHeaders() on the first + res_start, streams each chunk with res.write(), and ends with + res.end(). The 30 s timeout was extended to 120 s and cancelled the moment + res_start arrives. Legacy single-shot RelayResponse messages + still work via a backward-compat path in pending.ts. +

+
+ +
+

Roject integration — tunnel browser & chat

+

+ Two integration points added in Roject: +

+
    +
  • Browse button in rojo-settings-panel — calls + GET /api/rojos/tunnels/browse (Roject server proxies to + tunnel.rokojori.com/api/tunnels/available), renders a picker list; + clicking an entry fills the Tunnel ID field automatically.
  • +
  • Chat via tunnel — fixed baseURL to append + /v1 (/t/:tunnelId/v1); fixed auth token forwarding + (cookie OR Authorization Bearer header — dual-source pattern) so the Electron app + can use tunnels without a cookie.
  • +
  • TUNNEL_SERVER_URL env var used throughout; defaults to + https://tunnel.rokojori.com in production.
  • +
+
+ +
+

Client-side chunk animation

+

+ TCP batching through the relay delivers larger chunks than direct local streaming, + making the chat feel like it buffers instead of streams. Fixed client-side in + rojo-chat-panel.ts: a typeText() function splits any + chunk longer than 6 characters into 3-character pieces and awaits 18 ms between + each piece (~166 chars/sec). Chunks of ≤ 6 chars are displayed instantly, so + true single-token responses from a local direct LLM are unaffected. +

+
+ +
+

Key decisions

@@ -112,6 +193,32 @@

+
+

+ ts-node in production dependencies. Moving ts-node (and typescript) + from devDependencies to dependencies is the simplest way to ensure they survive + npm install --omit=dev on the server without a separate build step. +

+
+ +
+

+ Dual-source token extraction. The Roject server extracts the JWT + from the accessToken cookie first, then falls back to the + Authorization: Bearer header. This lets both browser-based (cookie) + and Electron-based (Bearer) clients use the tunnel chat without separate code paths. +

+
+ +
+

+ Client-side fake streaming instead of server changes. + Splitting large relay chunks in the browser is zero-risk and zero-latency overhead — + no server changes, no protocol changes. The animation runs entirely in the UI + and is transparent to the LLM backend. +

+
+