From a7b4fbcbc179dd51913f065dc2fe44d862ac5464 Mon Sep 17 00:00:00 2001 From: srojk34 <286497132+srojk34@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:49:38 +0300 Subject: [PATCH] fix(tui): guard /update against hosted dashboard mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /update calls dieWithCode(42) which tears down the gateway and hard-exits the Node process — the same PTY-killing path that /exit and /quit use. In the hosted dashboard chat there is no Python update wrapper to catch exit code 42, and the PTY death bricks the tab until a browser refresh. Mirror the DASHBOARD_TUI_MODE guard that #48882 added for /exit and /quit: refuse early with an explanatory message. --- .../src/__tests__/createSlashHandler.test.ts | 18 +++++++++++++++++- ui-tui/src/app/slash/commands/core.ts | 9 +++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 415dd4c0f3c..8f49dd9a513 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createSlashHandler } from '../app/createSlashHandler.js' import { getOverlayState, resetOverlayState } from '../app/overlayStore.js' -import { DASHBOARD_EXIT_DISABLED_MESSAGE } from '../app/slash/commands/core.js' +import { DASHBOARD_EXIT_DISABLED_MESSAGE, DASHBOARD_UPDATE_DISABLED_MESSAGE } from '../app/slash/commands/core.js' import { getUiState, patchUiState, resetUiState } from '../app/uiStore.js' import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js' @@ -118,6 +118,22 @@ describe('createSlashHandler', () => { vi.useRealTimers() }) + it('refuses /update in hosted dashboard chat instead of killing the PTY', () => { + vi.useFakeTimers() + envState.dashboardTuiMode = true + const ctx = buildCtx() + + expect(createSlashHandler(ctx)('/update')).toBe(true) + expect(ctx.session.dieWithCode).not.toHaveBeenCalled() + expect(ctx.gateway.gw.request).not.toHaveBeenCalled() + expect(ctx.transcript.sys).toHaveBeenCalledWith(DASHBOARD_UPDATE_DISABLED_MESSAGE) + + vi.advanceTimersByTime(150) + expect(ctx.session.dieWithCode).not.toHaveBeenCalled() + + vi.useRealTimers() + }) + it('routes /status to live session.status instead of slash worker', async () => { patchUiState({ sid: 'sid-abc' }) const rpc = vi.fn(() => Promise.resolve({ output: 'Hermes TUI Status' })) diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 7c5a79505ad..5c74eb3eb42 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -81,6 +81,9 @@ const DETAILS_SECTION_USAGE = 'usage: /details
[hidden|collapsed|expan export const DASHBOARD_EXIT_DISABLED_MESSAGE = 'exit is disabled in hosted dashboard chat — use /new to start a fresh session' +export const DASHBOARD_UPDATE_DISABLED_MESSAGE = + 'update is disabled in hosted dashboard chat — the hosted environment is managed separately' + export const coreCommands: SlashCommand[] = [ { help: 'list commands + hotkeys', @@ -140,6 +143,12 @@ export const coreCommands: SlashCommand[] = [ help: 'update Hermes Agent to the latest version (exits TUI)', name: 'update', run: (_arg, ctx) => { + if (DASHBOARD_TUI_MODE) { + ctx.transcript.sys(DASHBOARD_UPDATE_DISABLED_MESSAGE) + + return + } + ctx.transcript.sys('exiting TUI to run update...') // Exit code 42 signals the Python wrapper to exec `hermes update`. // Use dieWithCode for proper cleanup (gateway kill + Ink unmount).