tunnel: Electron agent, production deployment, streaming, Roject integration, chunk animation
— 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 <noreply@anthropic.com>
This commit is contained in:
parent
614176d2a9
commit
403499341f
|
|
@ -3,6 +3,22 @@ import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/
|
||||||
|
|
||||||
declare const markdownit: ( options?: Record<string, unknown> ) => { render: ( md: string ) => string };
|
declare const markdownit: ( options?: Record<string, unknown> ) => { render: ( md: string ) => string };
|
||||||
|
|
||||||
|
async function typeText( text: string, onPiece: ( piece: string ) => void ): Promise<void>
|
||||||
|
{
|
||||||
|
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<void>( r => setTimeout( r, DELAY_MS ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function extractRawText( node: Node ): string
|
function extractRawText( node: Node ): string
|
||||||
{
|
{
|
||||||
if ( node.nodeType === Node.TEXT_NODE ) return node.nodeValue ?? '';
|
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 };
|
const msg = JSON.parse( trimmed ) as { type: string; text?: string };
|
||||||
if ( msg.type === 'CHAT' && msg.text )
|
if ( msg.type === 'CHAT' && msg.text )
|
||||||
{
|
{
|
||||||
markdown += msg.text;
|
await typeText( msg.text, piece =>
|
||||||
|
{
|
||||||
|
markdown += piece;
|
||||||
assistantBubble.innerHTML = this._md!.render( markdown );
|
assistantBubble.innerHTML = this._md!.render( markdown );
|
||||||
history.scrollTop = history.scrollHeight;
|
history.scrollTop = history.scrollHeight;
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {}
|
catch {}
|
||||||
|
|
|
||||||
|
|
@ -168,16 +168,15 @@
|
||||||
<task-item class="yellow hide-content">
|
<task-item class="yellow hide-content">
|
||||||
<task-title>rokojori-tunnel — Phase 2</task-title>
|
<task-title>rokojori-tunnel — Phase 2</task-title>
|
||||||
<task-content>
|
<task-content>
|
||||||
Phase 1 complete: relay server, CRUD tunnel API, WebSocket agent endpoint,
|
Production-deployed at tunnel.rokojori.com. Complete so far:
|
||||||
HTTP proxy, test agent script. Tested end-to-end with local LLM (gemma4-coding
|
relay server, CRUD API, streaming WebSocket protocol (res_start / res_data / res_end),
|
||||||
on port 8900) — request relayed and response returned correctly.
|
HTTP proxy with SSE streaming, Electron Tunnel Agent (tray, login, tunnel list),
|
||||||
Service lives at C:\rokojori\projects\web-projects\tunnel.
|
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)
|
— Allowed users list enforcement (multi-user private access)
|
||||||
— GET /api/tunnels/available with ?purpose= filter
|
|
||||||
— Public access mode (no auth required on proxy route)
|
— 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
|
||||||
</task-content>
|
</task-content>
|
||||||
</task-item>
|
</task-item>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<header>
|
<header>
|
||||||
<p class="date">Wednesday, 16 July 2026</p>
|
<p class="date">Wednesday, 16 July 2026</p>
|
||||||
<h1>Session History</h1>
|
<h1>Session History</h1>
|
||||||
<p class="subtitle">Brainstormed and scaffolded rokojori-tunnel — user-based local tunneling service.</p>
|
<p class="subtitle">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.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -75,6 +75,87 @@
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Session 2 — Electron agent, deployment, streaming, Roject integration</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Electron Tunnel Agent app</h3>
|
||||||
|
<p>
|
||||||
|
Full Electron desktop app at <code>tunnel/electron-agent/</code> — system tray icon,
|
||||||
|
login window (email + password → account.rokojori.com), persistent token storage in
|
||||||
|
<code>userData/tokens.json</code>, 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 <code>TunnelAgent</code> class (WebSocket, auto-reconnect,
|
||||||
|
exponential back-off). Build: <code>npm run electron:dev</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Production deployment — tunnel.rokojori.com</h3>
|
||||||
|
<p>
|
||||||
|
Deployed to the same server as roject.rokojori.com. Key config:
|
||||||
|
</p>
|
||||||
|
<ul style="line-height:1.9;margin-top:0.75rem">
|
||||||
|
<li><code>deploy/nginx-tunnel.conf</code> — HTTP proxy with WebSocket upgrade for
|
||||||
|
<code>/api/agent/</code> (<code>proxy_read_timeout 3600s</code>), TLS via Let's Encrypt</li>
|
||||||
|
<li><code>deploy/tunnel-rokojori.service</code> — systemd unit; <code>EnvironmentFile</code>
|
||||||
|
points to <code>/opt/tunnel-rokojori/.env</code>; <code>PORT=3003</code>
|
||||||
|
(3002 was already taken by styles.rokojori.com)</li>
|
||||||
|
<li><code>ts-node</code> moved from <code>devDependencies</code> → <code>dependencies</code>
|
||||||
|
so <code>npm install --omit=dev</code> still installs it on the server</li>
|
||||||
|
<li><code>ExecStart</code> uses the local <code>node_modules/.bin/ts-node</code>
|
||||||
|
to avoid version mismatches with any globally installed npx wrapper</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Streaming relay protocol</h3>
|
||||||
|
<p>
|
||||||
|
Replaced the single-shot <code>RelayResponse</code> (collect all chunks, send one JSON blob)
|
||||||
|
with a three-message streaming protocol over the agent WebSocket:
|
||||||
|
<code>res_start</code> (status + headers), <code>res_data</code> (base64 chunk),
|
||||||
|
<code>res_end</code>. The proxy route calls <code>res.flushHeaders()</code> on the first
|
||||||
|
<code>res_start</code>, streams each chunk with <code>res.write()</code>, and ends with
|
||||||
|
<code>res.end()</code>. The 30 s timeout was extended to 120 s and cancelled the moment
|
||||||
|
<code>res_start</code> arrives. Legacy single-shot <code>RelayResponse</code> messages
|
||||||
|
still work via a backward-compat path in <code>pending.ts</code>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Roject integration — tunnel browser & chat</h3>
|
||||||
|
<p>
|
||||||
|
Two integration points added in Roject:
|
||||||
|
</p>
|
||||||
|
<ul style="line-height:1.9;margin-top:0.75rem">
|
||||||
|
<li><strong>Browse button</strong> in <code>rojo-settings-panel</code> — calls
|
||||||
|
<code>GET /api/rojos/tunnels/browse</code> (Roject server proxies to
|
||||||
|
<code>tunnel.rokojori.com/api/tunnels/available</code>), renders a picker list;
|
||||||
|
clicking an entry fills the Tunnel ID field automatically.</li>
|
||||||
|
<li><strong>Chat via tunnel</strong> — fixed <code>baseURL</code> to append
|
||||||
|
<code>/v1</code> (<code>/t/:tunnelId/v1</code>); fixed auth token forwarding
|
||||||
|
(cookie OR Authorization Bearer header — dual-source pattern) so the Electron app
|
||||||
|
can use tunnels without a cookie.</li>
|
||||||
|
<li><code>TUNNEL_SERVER_URL</code> env var used throughout; defaults to
|
||||||
|
<code>https://tunnel.rokojori.com</code> in production.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Client-side chunk animation</h3>
|
||||||
|
<p>
|
||||||
|
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
|
||||||
|
<code>rojo-chat-panel.ts</code>: a <code>typeText()</code> 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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Key decisions</h2>
|
<h2>Key decisions</h2>
|
||||||
|
|
||||||
|
|
@ -112,6 +193,32 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
<strong>ts-node in production dependencies.</strong> Moving ts-node (and typescript)
|
||||||
|
from devDependencies to dependencies is the simplest way to ensure they survive
|
||||||
|
<code>npm install --omit=dev</code> on the server without a separate build step.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
<strong>Dual-source token extraction.</strong> The Roject server extracts the JWT
|
||||||
|
from the <code>accessToken</code> cookie first, then falls back to the
|
||||||
|
<code>Authorization: Bearer</code> header. This lets both browser-based (cookie)
|
||||||
|
and Electron-based (Bearer) clients use the tunnel chat without separate code paths.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p>
|
||||||
|
<strong>Client-side fake streaming instead of server changes.</strong>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3><a href="2026/07-July/16-Wednesday/index.html">Wednesday, 16 July 2026</a></h3>
|
<h3><a href="2026/07-July/16-Wednesday/index.html">Wednesday, 16 July 2026</a></h3>
|
||||||
<p>Brainstormed and scaffolded rokojori-tunnel — user-based local tunneling service; Phase 1 relay server built and tested end-to-end with local LLM (gemma4-coding on port 8900).</p>
|
<p>rokojori-tunnel: Phase 1 relay server, Electron Tunnel Agent app, production deployment to tunnel.rokojori.com, streaming relay protocol (res_start/res_data/res_end), Roject browse-tunnels UI, tunnel-backed LLM chat, and client-side chunk animation for smooth streaming appearance.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
|
|
@ -84,17 +84,27 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Ecosystem — tunnel.rokojori.com</h3>
|
<h3>Ecosystem — tunnel.rokojori.com</h3>
|
||||||
<p>
|
<p>
|
||||||
A user-based local tunneling service (in development at
|
A user-based local tunneling service live at <code>tunnel.rokojori.com</code>
|
||||||
<code>C:\rokojori\projects\web-projects\tunnel</code>, planned to live at
|
(source: <code>C:\rokojori\projects\web-projects\tunnel</code>). Exposes local
|
||||||
<code>tunnel.rokojori.com</code>). Exposes local services — LLMs, Stable
|
services — LLMs, Stable Diffusion, language servers — to authorised rokojori
|
||||||
Diffusion, language servers — to authorised rokojori users over the internet,
|
users over the internet, routed through a relay server via a persistent WebSocket
|
||||||
routed through a relay server via a persistent WebSocket agent connection.
|
agent connection. Each tunnel is owned by a rokojori account and carries metadata
|
||||||
Each tunnel is owned by a rokojori account and carries metadata (name, purpose
|
(name, purpose tag, description, access mode, allowed users).
|
||||||
tag, description, access mode, allowed users) so apps can discover and select
|
</p>
|
||||||
tunnels via <code>GET /api/tunnels/available?purpose=</code>.
|
<p style="margin-top:0.75rem">
|
||||||
Phase 1 (relay server, CRUD API, HTTP proxy, test agent) is complete and
|
The <strong>Electron Tunnel Agent</strong> (<code>electron-agent/</code>) is a
|
||||||
tested end-to-end. Phase 2 (multi-user access, discovery API, public mode)
|
Windows system-tray app: login via account.rokojori.com, a tunnel list with
|
||||||
is in progress.
|
Start / Stop / Delete / Create, and a green status dot when the agent WebSocket
|
||||||
|
is connected. Tokens are persisted in <code>userData/tokens.json</code>.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
The relay uses a three-message streaming protocol over the agent WebSocket:
|
||||||
|
<code>res_start</code> (status + headers), <code>res_data</code> (base64 chunk),
|
||||||
|
<code>res_end</code> — so SSE responses from local LLMs stream through the relay
|
||||||
|
without buffering. Roject integrates via a <em>Browse Tunnels</em> button in
|
||||||
|
<code>rojo-settings-panel</code> and forwards LLM chat through the selected
|
||||||
|
tunnel. The TUNNEL_SERVER_URL env var controls the target in all environments.
|
||||||
|
Phase 2 (multi-user allowed list enforcement, public access mode) is still in progress.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue