From 6a854bc8edad688decdfc76d97278b5784646ad8 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 16 May 2026 21:38:53 -0500 Subject: [PATCH] fix(desktop): trim sidebar terminal startup spacer Drop zsh's initial spacer row before writing the first terminal prompt so new sidebar terminal sessions do not open with a selectable blank line. --- apps/desktop/src/app/artifacts/index.tsx | 2 +- .../terminal/use-terminal-session.ts | 102 +++++++++++++++++- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/artifacts/index.tsx b/apps/desktop/src/app/artifacts/index.tsx index 147a075fd2c..f53366cb520 100644 --- a/apps/desktop/src/app/artifacts/index.tsx +++ b/apps/desktop/src/app/artifacts/index.tsx @@ -7,7 +7,6 @@ import { PageLoader } from '@/components/page-loader' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import { CopyButton } from '@/components/ui/copy-button' -import { TextTab, TextTabMeta } from '@/components/ui/text-tab' import { Pagination, PaginationButton, @@ -17,6 +16,7 @@ import { PaginationNext, PaginationPrevious } from '@/components/ui/pagination' +import { TextTab, TextTabMeta } from '@/components/ui/text-tab' import { getSessionMessages, listSessions } from '@/hermes' import { sessionTitle } from '@/lib/chat-runtime' import { ExternalLink, ExternalLinkIcon, hostPathLabel, urlSlugTitleLabel, useLinkTitle } from '@/lib/external-link' diff --git a/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts b/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts index cedb05416b3..8935da82759 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +++ b/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts @@ -12,6 +12,82 @@ import { isAddSelectionShortcut, terminalSelectionAnchor, terminalSelectionLabel type TerminalStatus = 'closed' | 'open' | 'starting' +function readEscapeSequence(data: string, index: number) { + if (data.charCodeAt(index) !== 0x1b || index + 1 >= data.length) { + return null + } + + const kind = data[index + 1] + + if (kind === '[') { + for (let i = index + 2; i < data.length; i += 1) { + const code = data.charCodeAt(i) + + if (code >= 0x40 && code <= 0x7e) { + return data.slice(index, i + 1) + } + } + } + + if (kind === ']') { + for (let i = index + 2; i < data.length; i += 1) { + if (data.charCodeAt(i) === 0x07) { + return data.slice(index, i + 1) + } + + if (data.charCodeAt(i) === 0x1b && data[i + 1] === '\\') { + return data.slice(index, i + 2) + } + } + } + + return data.slice(index, Math.min(index + 2, data.length)) +} + +function stripEscapeSequences(data: string) { + let index = 0 + let text = '' + + while (index < data.length) { + const sequence = readEscapeSequence(data, index) + + if (sequence) { + index += sequence.length + } else { + text += data[index] + index += 1 + } + } + + return text +} + +function isStartupSpacer(data: string) { + const text = stripEscapeSequences(data).replace(/[\s\r\n]/g, '') + + return text === '' || text === '%' +} + +function stripInitialPromptGap(data: string) { + let index = 0 + let prefix = '' + + while (index < data.length) { + const sequence = readEscapeSequence(data, index) + + if (sequence) { + prefix += sequence + index += sequence.length + } else if (data[index] === '\r' || data[index] === '\n') { + index += 1 + } else { + return prefix + data.slice(index) + } + } + + return prefix +} + interface UseTerminalSessionOptions { cwd: string onAddSelectionToChat: (text: string, label?: string) => void @@ -98,6 +174,7 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes let disposed = false const cleanup: Array<() => void> = [] + let lastSentSize: { cols: number; rows: number } | null = null const term = new Terminal({ allowProposedApi: true, @@ -134,7 +211,8 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes const id = sessionIdRef.current - if (id) { + if (id && (lastSentSize?.cols !== term.cols || lastSentSize.rows !== term.rows)) { + lastSentSize = { cols: term.cols, rows: term.rows } void terminalApi.resize(id, { cols: term.cols, rows: term.rows }) } } @@ -190,6 +268,7 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes } sessionIdRef.current = session.id + lastSentSize = { cols: term.cols, rows: term.rows } shellNameRef.current = session.shell || 'shell' setShellName(session.shell || 'shell') @@ -203,8 +282,27 @@ export function useTerminalSession({ cwd, onAddSelectionToChat }: UseTerminalSes } setStatus('open') + let wrotePromptContent = false + cleanup.push( - terminalApi.onData(session.id, data => term.write(data)), + terminalApi.onData(session.id, data => { + if (wrotePromptContent) { + term.write(data) + + return + } + + if (isStartupSpacer(data)) { + return + } + + const next = stripInitialPromptGap(data) + + if (next) { + wrotePromptContent = true + term.write(next) + } + }), terminalApi.onExit(session.id, sessionExit => { const { code, signal } = sessionExit setStatus('closed')