# NousResearch/hermes-agent · TUI compound prompt separator spacing fixes

## Open now

- Nothing open on GitHub.

These items address a bug where compound prompt symbols lose their separator space or gutter width in the TUI, with fixes to preserve and correctly scale that spacing.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #19340. Verify it addresses your case.
- **Recommended:** read PR #19340 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-19340`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #18996: Bug: TUI compound prompt symbols lose separator space in sent messages

## Prior & competing PRs

- **#19340: fix(tui): preserve prompt separator width**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `ui-tui/src/__tests__/messages.test.ts`, `ui-tui/src/__tests__/virtualHeights.test.ts`, `ui-tui/src/app/useMainApp.ts`, `ui-tui/src/components/messageLine.tsx`, `ui-tui/src/lib/inputMetrics.ts`, `ui-tui/src/lib/virtualHeights.ts`
  - fix hunks:
  - `ui-tui/src/__tests__/messages.test.ts`

    ```diff
    @@ -1,7 +1,13 @@
    +import { renderSync } from '@hermes/ink'
    +import React from 'react'
    +import { PassThrough } from 'stream'
     import { describe, expect, it } from 'vitest'
     
    +import { MessageLine } from '../components/messageLine.js'
     import { toTranscriptMessages } from '../domain/messages.js'
     import { upsert } from '../lib/messages.js'
    +import { stripAnsi } from '../lib/text.js'
    +import { DEFAULT_THEME } from '../theme.js'
     
     describe('toTranscriptMessages', () => {
       it('preserves assistant tool-call rows so resume does not drop prior turns', () => {
    ```

    ```diff
    @@ -21,6 +27,50 @@ describe('toTranscriptMessages', () => {
       })
     })
     
    +describe('MessageLine', () => {
    +  it('preserves a separator after compound user prompt glyphs in transcript rows', () => {
    +    const stdout = new PassThrough()
    +    const stdin = new PassThrough()
    +    const stderr = new PassThrough()
    +    let output = ''
    +
    +    Object.assign(stdout, { columns: 80, isTTY: false, rows: 24 })
    +    Object.assign(stdin, { isTTY: false })
    +    Object.assign(stderr, { isTTY: false })
    +    stdout.on('data', chunk => {
    +      output += chunk.toString()
    +    })
    +
    +    const t = {
    +      ...DEFAULT_THEME,
    +      brand: { ...DEFAULT_THEME.brand, prompt: 'Ψ >' }
    +    }
    +
    +    const instance = renderSync(
    +      React.createElement(MessageLine, {
    +        cols: 80,
    +        msg: { role: 'user', text: 'Okay' },
    +        t
    +      }),
    +      {
    +        patchConsole: false,
    +        stderr: stderr as NodeJS.WriteStream,
    +        stdin: stdin as NodeJS.ReadStream,
    +        stdout: stdout as NodeJS.WriteStream
    +      }
    +    )
    +
    +    instance.unmount()
    +    instance.cleanup()
    +
    +    const renderedLine = stripAnsi(output)
    +      .split('\n')
    +      .find(line => line.includes('Okay'))
    +
    +    expect(renderedLine).toContain('Ψ > Okay')
    +  })
    +})
    +
     describe('upsert', () => {
       it('appends when last role differs', () => {
         expect(upsert([{ role: 'user', text: 'hi' }], 'assistant', 'hello')).toHaveLength(2)
    ```
  - `ui-tui/src/__tests__/virtualHeights.test.ts`

    ```diff
    @@ -17,6 +17,13 @@ describe('virtual height estimates', () => {
         expect(estimatedMsgHeight(msg, 35, { compact: false, details: false })).toBeGreaterThan(5)
       })
     
    +  it('uses compound user prompt width when estimating user message wrapping', () => {
    +    const msg: Msg = { role: 'user', text: 'x'.repeat(21) }
    +
    +    expect(estimatedMsgHeight(msg, 26, { compact: false, details: false, userPrompt: '❯' })).toBe(3)
    +    expect(estimatedMsgHeight(msg, 26, { compact: false, details: false, userPrompt: 'Ψ >' })).toBe(4)
    +  })
    +
       it('includes detail sections when visible', () => {
         const msg: Msg = { role: 'assistant', text: 'ok', thinking: 'line 1\nline 2', tools: ['Tool A', 'Tool B'] }
     
    ```
  - `ui-tui/src/app/useMainApp.ts`

    ```diff
    @@ -17,6 +17,7 @@ import type {
     import { useGitBranch } from '../hooks/useGitBranch.js'
     import { useVirtualHistory } from '../hooks/useVirtualHistory.js'
     import { appendTranscriptMessage } from '../lib/messages.js'
    +import { composerPromptWidth } from '../lib/inputMetrics.js'
     import { isMac } from '../lib/platform.js'
     import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
     import { terminalParityHints } from '../lib/terminalParity.js'
    ```

    ```diff
    @@ -244,7 +245,8 @@ export function useMainApp(gw: GatewayClient) {
       }, [ui.detailsMode, ui.detailsModeCommandOverride, ui.sections])
     
       const detailsVisible = detailsLayoutKey !== 'hidden:hidden'
    -  const heightCacheKey = `${ui.sid ?? 'draft'}:${cols}:${ui.compact ? '1' : '0'}:${detailsLayoutKey}`
    +  const userPromptWidth = composerPromptWidth(ui.theme.brand.prompt)
    +  const heightCacheKey = `${ui.sid ?? 'draft'}:${cols}:${userPromptWidth}:${ui.compact ? '1' : '0'}:${detailsLayoutKey}`
     
       const heightCache = useMemo(() => {
         let cache = heightCachesRef.current.get(heightCacheKey)
    ```

    ```diff
    @@ -266,9 +268,10 @@ export function useMainApp(gw: GatewayClient) {
           estimatedMsgHeight(virtualRows[index]!.msg, cols, {
             compact: ui.compact,
             details: detailsVisible,
    -        limitHistory: index < virtualRows.length - FULL_RENDER_TAIL_ITEMS
    +        limitHistory: index < virtualRows.length - FULL_RENDER_TAIL_ITEMS,
    +        userPrompt: ui.theme.brand.prompt
           }),
    -    [cols, detailsVisible, ui.compact, virtualRows]
    +    [cols, detailsVisible, ui.compact, ui.theme.brand.prompt, virtualRows]
       )
     
       const syncHeightCache = useCallback(
    ```
  - `ui-tui/src/components/messageLine.tsx`

    ```diff
    @@ -5,6 +5,7 @@ import { LONG_MSG } from '../config/limits.js'
     import { sectionMode } from '../domain/details.js'
     import { userDisplay } from '../domain/messages.js'
     import { ROLE } from '../domain/roles.js'
    +import { transcriptBodyWidth, transcriptGutterWidth } from '../lib/inputMetrics.js'
     import {
       boundedHistoryRenderText,
       boundedLiveRenderText,
    ```

    ```diff
    @@ -95,6 +96,7 @@ export const MessageLine = memo(function MessageLine({
       }
     
       const { body, glyph, prefix } = ROLE[msg.role](t)
    +  const gutterWidth = transcriptGutterWidth(msg.role, t.brand.prompt)
     
       const showDetails =
         (toolsMode !== 'hidden' && Boolean(msg.tools?.length)) || (thinkingMode !== 'hidden' && Boolean(thinking))
    ```

    ```diff
    @@ -163,13 +165,13 @@ export const MessageLine = memo(function MessageLine({
           )}
     
           <Box>
    -        <NoSelect flexShrink={0} fromLeftEdge width={3}>
    +        <NoSelect flexShrink={0} fromLeftEdge width={gutterWidth}>
               <Text bold={msg.role === 'user'} color={prefix}>
                 {glyph}{' '}
               </Text>
             </NoSelect>
     
    -        <Box width={Math.max(20, cols - 5)}>{content}</Box>
    +        <Box width={transcriptBodyWidth(cols, msg.role, t.brand.prompt)}>{content}</Box>
           </Box>
         </Box>
       )
    ```
  - `ui-tui/src/lib/inputMetrics.ts`

    ```diff
    @@ -1,5 +1,7 @@
     import { stringWidth } from '@hermes/ink'
     
    +import type { Role } from '../types.js'
    +
     export const COMPOSER_PROMPT_GAP_WIDTH = 1
     
     let _seg: Intl.Segmenter | null = null
    ```

    ```diff
    @@ -162,6 +164,14 @@ export function composerPromptWidth(promptText: string) {
       return Math.max(1, stringWidth(promptText)) + COMPOSER_PROMPT_GAP_WIDTH
     }
     
    +export function transcriptGutterWidth(role: Role, userPrompt: string) {
    +  return role === 'user' ? composerPromptWidth(userPrompt) : 3
    +}
    +
    +export function transcriptBodyWidth(totalCols: number, role: Role, userPrompt: string) {
    +  return Math.max(20, totalCols - transcriptGutterWidth(role, userPrompt) - 2)
    +}
    +
     export function stableComposerColumns(totalCols: number, promptWidth: number) {
       // Physical render/wrap width. Always reserve outer composer padding and
       // prompt prefix. Only reserve the transcript scrollbar gutter when the
    ```
  - `ui-tui/src/lib/virtualHeights.ts`

    ```diff
    @@ -1,5 +1,6 @@
     import type { Msg } from '../types.js'
     
    +import { transcriptBodyWidth } from './inputMetrics.js'
     import { boundedHistoryRenderText } from './text.js'
     
     const hashText = (text: string) => {
    ```

    ```diff
    @@ -38,7 +39,12 @@ export const wrappedLines = (text: string, width: number) => {
     export const estimatedMsgHeight = (
       msg: Msg,
       cols: number,
    -  { compact, details, limitHistory = false }: { compact: boolean; details: boolean; limitHistory?: boolean }
    +  {
    +    compact,
    +    details,
    +    limitHistory = false,
    +    userPrompt = ''
    +  }: { compact: boolean; details: boolean; limitHistory?: boolean; userPrompt?: string }
     ) => {
       if (msg.kind === 'intro') {
         return msg.info?.version ? 9 : 5
    ```

    ```diff
    @@ -56,7 +62,7 @@ export const estimatedMsgHeight = (
         return Math.max(2, msg.todos.length + 2)
       }
     
    -  const bodyWidth = Math.max(20, cols - 5)
    +  const bodyWidth = transcriptBodyWidth(cols, msg.role, userPrompt)
       const text = msg.role === 'assistant' && limitHistory ? boundedHistoryRenderText(msg.text) : msg.text
       let h = wrappedLines(text || ' ', bodyWidth)
     
    ```
- **#19010: fix(tui): scale transcript prompt gutter for compound prompt symbols**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `0ce1b9fe20a53459b37b7ab27dcb88336dbea781`, `#19340`

## Fix status

- Merged to main in PR #19340 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-04`.
- Subsystem: `other`
