From f082cdc63bd0de90f8b3da1df6071e91ab545831 Mon Sep 17 00:00:00 2001 From: Peter Buchegger Date: Thu, 4 Jun 2026 19:50:02 +0200 Subject: [PATCH 1/4] fix(websocket): reset unmountedRef on each effect re-run so token refresh reconnects (#721) The effect cleanup sets unmountedRef.current = true to prevent reconnects after the provider unmounts. Without an inverse reset at the start of the effect, re-running the effect (e.g. when the auth token rotates) leaves the ref true, and connect() short-circuits at its unmounted guard. The socket then stays permanently disconnected for the lifetime of the provider. Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com> --- src/contexts/WebSocketContext.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/contexts/WebSocketContext.tsx b/src/contexts/WebSocketContext.tsx index 116da6b9..456c0761 100644 --- a/src/contexts/WebSocketContext.tsx +++ b/src/contexts/WebSocketContext.tsx @@ -36,8 +36,12 @@ const useWebSocketProviderState = (): WebSocketContextType => { const { token } = useAuth(); useEffect(() => { + // The cleanup below sets unmountedRef = true. Without this reset, every + // re-run of the effect (e.g. on token refresh) would short-circuit connect() + // at its unmounted guard and leave the socket permanently disconnected. + unmountedRef.current = false; connect(); - + return () => { unmountedRef.current = true; if (reconnectTimeoutRef.current) { From 96b16b42e4f807d04ec743a5a4117a37a3f5e0d9 Mon Sep 17 00:00:00 2001 From: ehsanmim <115899665+ehsanmim@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:57:24 +0200 Subject: [PATCH 2/4] fix(vite): proxy /plugin-ws WebSocket requests to the backend in dev (#757) Plugin WebSocket connections (e.g. the official Terminal plugin) hang in `npm run dev` because Vite proxies /api, /ws, and /shell but not /plugin-ws/*. Production is unaffected because the same Express server serves both the frontend and the WS gateway. Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com> --- vite.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vite.config.js b/vite.config.js index 61967cbe..664fd307 100755 --- a/vite.config.js +++ b/vite.config.js @@ -37,6 +37,10 @@ export default defineConfig(({ mode }) => { '/shell': { target: `ws://${proxyHost}:${serverPort}`, ws: true + }, + '/plugin-ws': { + target: `ws://${proxyHost}:${serverPort}`, + ws: true } } }, From 2edfef2e3f4271c29ae8670df9dd382a9eef7c3c Mon Sep 17 00:00:00 2001 From: Vojtech <119944107+cvrysanek@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:07:59 +0400 Subject: [PATCH 3/4] fix(websocket): add 30s server-side heartbeat to prevent proxy idle disconnects (#770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WebSocket gateway never sent ping frames, so any reverse proxy with an idle timeout (Cloudflare Tunnel ~100s, AWS ALB 60s, nginx 60s, etc.) would silently tear down /shell, /ws and /plugin-ws/* connections after the idle window. The UI reconnects automatically but users see a "Connecting to shell" toast every 1–3 minutes during normal use and any in-flight PTY/chat traffic can race the reconnect. Schedule a 30s ws.ping() per connection at the gateway level, cleared on close/error. ping/pong counts as protocol activity for all proxies that implement WebSocket correctly, so this single change covers every deployment topology without per-proxy tuning. Fixes #769 Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com> --- .../services/websocket-server.service.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/modules/websocket/services/websocket-server.service.ts b/server/modules/websocket/services/websocket-server.service.ts index 7e5c12e4..2ba2ec6e 100644 --- a/server/modules/websocket/services/websocket-server.service.ts +++ b/server/modules/websocket/services/websocket-server.service.ts @@ -31,6 +31,24 @@ export function createWebSocketServer( }); wss.on('connection', (ws, request) => { + // Keep WebSocket alive across reverse-proxy idle timeouts (Cloudflare ~100s, + // AWS ALB 60s, nginx 60s, etc.). Without app-level pings these connections + // are silently torn down even when the UI is active, causing repeated + // reconnect cycles. ws library heartbeat is opt-in. + const HEARTBEAT_INTERVAL_MS = 30_000; + const heartbeat = setInterval(() => { + if (ws.readyState === ws.OPEN) { + try { + ws.ping(); + } catch { + // socket may have been closed concurrently — interval will be cleared below + } + } + }, HEARTBEAT_INTERVAL_MS); + const stopHeartbeat = () => clearInterval(heartbeat); + ws.on('close', stopHeartbeat); + ws.on('error', stopHeartbeat); + const incomingRequest = request as AuthenticatedWebSocketRequest; const url = incomingRequest.url ?? '/'; const pathname = new URL(url, 'http://localhost').pathname; From fa9eaf5573a6f870a19fb62ab430ffd87c466582 Mon Sep 17 00:00:00 2001 From: Reza Moghaddam Date: Thu, 4 Jun 2026 22:54:07 +0330 Subject: [PATCH 4/4] feat(chat): auto-detect text direction for RTL languages (#729) Add dir="auto" to chat message content and composer textarea so Persian and Arabic text automatically renders right-to-left while English and other LTR text remains unaffected. Co-authored-by: Haile <118998054+blackmammoth@users.noreply.github.com> --- src/components/chat/view/subcomponents/ChatComposer.tsx | 1 + src/components/chat/view/subcomponents/MessageComponent.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx index c447bc19..f041b773 100644 --- a/src/components/chat/view/subcomponents/ChatComposer.tsx +++ b/src/components/chat/view/subcomponents/ChatComposer.tsx @@ -295,6 +295,7 @@ export default function ChatComposer({
-
+
{message.content}
{message.images && message.images.length > 0 && ( @@ -405,7 +405,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o ) : ( -
+
{/* Reasoning accordion */} {showThinking && message.reasoning && (