From 3f0e9849e7a2753931ef32c624cae33a7461e653 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:29:19 +0530 Subject: [PATCH] refactor(tui): reuse DASHBOARD_TUI_MODE for hosted /exit guard Follow-up to the salvaged hosted /exit fix. Instead of a separate 4-env-var fingerprint (HERMES_TUI_INLINE + /opt/data HERMES_HOME + HERMES_WRITE_SAFE_ROOT + HERMES_DISABLE_LAZY_INSTALLS), gate /exit and /quit on the existing DASHBOARD_TUI_MODE flag (HERMES_TUI_DASHBOARD) that the keyboard idle-exit (useInputHandlers) and SIGINT-ignore (entry.tsx) paths already use. One hosted detection mechanism instead of two divergent ones. Extract the refusal text to an exported DASHBOARD_EXIT_DISABLED_MESSAGE so the test asserts the same source of truth as production (no change-detector on the literal). Test mocks only the DASHBOARD_TUI_MODE export via importActual so the other env exports stay real. --- .../src/__tests__/createSlashHandler.test.ts | 35 +++++++++++-------- ui-tui/src/app/slash/commands/core.ts | 30 ++++++++-------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index c0247795af3..415dd4c0f3c 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -2,17 +2,30 @@ 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 { getUiState, patchUiState, resetUiState } from '../app/uiStore.js' import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js' +// DASHBOARD_TUI_MODE resolves once at module load from HERMES_TUI_DASHBOARD, +// so toggling process.env in a test body can't move it. Mock just that one +// export (everything else stays real) and flip the holder per test. +const envState = { dashboardTuiMode: false } +vi.mock('../config/env.js', async importActual => { + const actual = await importActual() + + return { + ...actual, + get DASHBOARD_TUI_MODE() { + return envState.dashboardTuiMode + } + } +}) + describe('createSlashHandler', () => { beforeEach(() => { resetOverlayState() resetUiState() - delete process.env.HERMES_TUI_INLINE - delete process.env.HERMES_HOME - delete process.env.HERMES_WRITE_SAFE_ROOT - delete process.env.HERMES_DISABLE_LAZY_INSTALLS + envState.dashboardTuiMode = false }) it('opens the unified sessions overlay for /resume', () => { @@ -73,25 +86,17 @@ describe('createSlashHandler', () => { }) it('keeps hosted dashboard chat alive for /exit', () => { - process.env.HERMES_TUI_INLINE = '1' - process.env.HERMES_HOME = '/opt/data/profiles/worker' - process.env.HERMES_WRITE_SAFE_ROOT = '/opt/data' - process.env.HERMES_DISABLE_LAZY_INSTALLS = '1' + envState.dashboardTuiMode = true const ctx = buildCtx() expect(createSlashHandler(ctx)('/exit')).toBe(true) expect(ctx.session.die).not.toHaveBeenCalled() expect(ctx.gateway.gw.request).not.toHaveBeenCalled() - expect(ctx.transcript.sys).toHaveBeenCalledWith( - 'exit is disabled in hosted dashboard chat — use /new to start a fresh session' - ) + expect(ctx.transcript.sys).toHaveBeenCalledWith(DASHBOARD_EXIT_DISABLED_MESSAGE) }) it('keeps /quit available outside hosted dashboard chat', () => { - process.env.HERMES_TUI_INLINE = '1' - process.env.HERMES_HOME = '/Users/example/.hermes' - process.env.HERMES_WRITE_SAFE_ROOT = '/Users/example/.hermes' - process.env.HERMES_DISABLE_LAZY_INSTALLS = '1' + envState.dashboardTuiMode = false const ctx = buildCtx() expect(createSlashHandler(ctx)('/quit')).toBe(true) diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index b5d72cf7712..7c5a79505ad 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -1,6 +1,6 @@ import { forceRedraw, type MouseTrackingMode } from '@hermes/ink' -import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js' +import { DASHBOARD_TUI_MODE, NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js' import { dailyFortune, randomFortune } from '../../../content/fortunes.js' import { HOTKEYS } from '../../../content/hotkeys.js' import { isSectionName, nextDetailsMode, parseDetailsMode, SECTION_NAMES } from '../../../domain/details.js' @@ -76,19 +76,10 @@ const DETAILS_USAGE = const DETAILS_SECTION_USAGE = 'usage: /details
[hidden|collapsed|expanded|reset]' -const truthyEnv = (v?: string) => /^(?:1|true|yes|on)$/i.test((v ?? '').trim()) - -const hostedInlineDashboardChat = () => { - const hermesHome = (process.env.HERMES_HOME ?? '').trim() - const hostedHome = hermesHome === '/opt/data' || hermesHome.startsWith('/opt/data/') - - return ( - process.env.HERMES_TUI_INLINE === '1' && - hostedHome && - process.env.HERMES_WRITE_SAFE_ROOT === '/opt/data' && - truthyEnv(process.env.HERMES_DISABLE_LAZY_INSTALLS) - ) -} +// Shown when /exit or /quit is refused in the hosted dashboard chat. Kept as a +// constant so the test asserts against the same source of truth as production. +export const DASHBOARD_EXIT_DISABLED_MESSAGE = + 'exit is disabled in hosted dashboard chat — use /new to start a fresh session' export const coreCommands: SlashCommand[] = [ { @@ -128,8 +119,15 @@ export const coreCommands: SlashCommand[] = [ help: 'exit hermes', name: 'quit', run: (_arg, ctx) => { - if (hostedInlineDashboardChat()) { - ctx.transcript.sys('exit is disabled in hosted dashboard chat — use /new to start a fresh session') + // In the hosted dashboard chat there is no in-page restart path after + // the PTY child exits, so quitting bricks the tab until a refresh. The + // keyboard idle-exit (Ctrl+C / Ctrl+D) and SIGINT handling already refuse + // to die in this mode (see useInputHandlers + entry.tsx); gate /exit and + // /quit on the same DASHBOARD_TUI_MODE flag. Unlike the keyboard path + // (which auto-starts a fresh chat), the explicit quit command refuses and + // instructs the user to run /new themselves. + if (DASHBOARD_TUI_MODE) { + ctx.transcript.sys(DASHBOARD_EXIT_DISABLED_MESSAGE) return }