mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(desktop): stop injecting ctrl-l into terminal startup
Remove the prompt-gap cleanup that sent Ctrl-L into the user's shell; it could render as literal ^L and create the exact top-line gap it was meant to hide. Keep first-prompt cleanup renderer-side only, and parse short ESC charset sequences so the initial newline stripper does not disarm early. Also add a Close all action to the terminal tab context menu.
This commit is contained in:
parent
6c52e4a318
commit
1a1e00f37e
8 changed files with 41 additions and 50 deletions
|
|
@ -19,6 +19,7 @@ import { setTerminalTakeover } from '../store'
|
|||
import {
|
||||
$activeTerminalId,
|
||||
$terminals,
|
||||
closeAllTerminals,
|
||||
closeOtherTerminals,
|
||||
closeTerminal,
|
||||
createTerminal,
|
||||
|
|
@ -167,6 +168,7 @@ function TerminalRailItem({ active, canCloseOthers, index, term, toggleHint }: T
|
|||
<ContextMenuItem disabled={!canCloseOthers} onSelect={() => closeOtherTerminals(term.id)}>
|
||||
{t.rightSidebar.terminalCloseOthers}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={closeAllTerminals}>{t.rightSidebar.terminalCloseAll}</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => setTerminalTakeover(false)}>{t.rightSidebar.terminalHide}</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
|
|
|
|||
|
|
@ -110,7 +110,10 @@ export function cycleTerminal(direction: 1 | -1): void {
|
|||
return
|
||||
}
|
||||
|
||||
const current = Math.max(0, list.findIndex(term => term.id === $activeTerminalId.get()))
|
||||
const current = Math.max(
|
||||
0,
|
||||
list.findIndex(term => term.id === $activeTerminalId.get())
|
||||
)
|
||||
|
||||
$activeTerminalId.set(list[(current + direction + list.length) % list.length].id)
|
||||
}
|
||||
|
|
@ -162,6 +165,16 @@ export function closeActiveTerminal(): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function closeAllTerminals(): void {
|
||||
if ($terminals.get().length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
$terminals.set([])
|
||||
$activeTerminalId.set(null)
|
||||
setTerminalTakeover(false)
|
||||
}
|
||||
|
||||
export function closeOtherTerminals(id: string): void {
|
||||
const keep = $terminals.get().find(term => term.id === id)
|
||||
|
||||
|
|
@ -174,7 +187,9 @@ export function closeOtherTerminals(id: string): void {
|
|||
export function renameTerminal(id: string, title: string): void {
|
||||
const trimmed = title.trim()
|
||||
|
||||
$terminals.set($terminals.get().map(term => (term.id === id ? { ...term, title: trimmed || term.title, auto: false } : term)))
|
||||
$terminals.set(
|
||||
$terminals.get().map(term => (term.id === id ? { ...term, title: trimmed || term.title, auto: false } : term))
|
||||
)
|
||||
}
|
||||
|
||||
/** A live terminal reports its resolved shell; adopt it as the label only while
|
||||
|
|
|
|||
|
|
@ -66,6 +66,14 @@ function readEscapeSequence(data: string, index: number) {
|
|||
}
|
||||
}
|
||||
|
||||
// Character-set and other short ESC forms are three bytes (e.g. ESC ( B).
|
||||
// Treating only ESC+( as a sequence leaves the final selector ("B") as
|
||||
// printable text, which disarms the initial prompt-gap stripper before it can
|
||||
// eat the shell's leading newline.
|
||||
if (['(', ')', '*', '+', '-', '.', '/'].includes(kind) && index + 2 < data.length) {
|
||||
return data.slice(index, index + 3)
|
||||
}
|
||||
|
||||
return data.slice(index, Math.min(index + 2, data.length))
|
||||
}
|
||||
|
||||
|
|
@ -419,18 +427,9 @@ export function useTerminalSession({ id, cwd, active, onAddSelectionToChat, onSh
|
|||
host.removeEventListener('drop', onDrop)
|
||||
})
|
||||
|
||||
// A fresh prompt should sit at the top. Every resize SIGWINCHes the shell,
|
||||
// which reprints its prompt and can leave stale blank rows above it. While
|
||||
// the session is pristine (nothing run yet) we ask the shell to clear +
|
||||
// redraw via Ctrl-L (\f) after the resize settles. Ctrl-L preserves
|
||||
// multi-line prompts (term.clear() would drop all but the cursor row) and we
|
||||
// stop the moment real output exists, so command scrollback is never wiped.
|
||||
let promptPristine = true
|
||||
let gapCleanupTimer = 0
|
||||
|
||||
// While armed, strip leading blank rows so the prompt lands at the very top
|
||||
// (no starship `add_newline` gap). Re-armed before each Ctrl-L redraw so the
|
||||
// resize cleanup doesn't reintroduce the blank line.
|
||||
// While armed, strip leading blank rows so the first prompt lands at the
|
||||
// very top (no starship `add_newline` gap). Do this only on renderer output:
|
||||
// never inject Ctrl-L or other cleanup keystrokes into the user's shell.
|
||||
let stripLeading = true
|
||||
|
||||
const armedWrite = (data: string) => {
|
||||
|
|
@ -459,35 +458,6 @@ export function useTerminalSession({ id, cwd, active, onAddSelectionToChat, onSh
|
|||
term.write(next)
|
||||
}
|
||||
|
||||
const scheduleGapCleanup = () => {
|
||||
if (!promptPristine) {
|
||||
return
|
||||
}
|
||||
|
||||
if (gapCleanupTimer) {
|
||||
window.clearTimeout(gapCleanupTimer)
|
||||
}
|
||||
|
||||
gapCleanupTimer = window.setTimeout(() => {
|
||||
gapCleanupTimer = 0
|
||||
const id = sessionIdRef.current
|
||||
|
||||
if (disposed || !id || !promptPristine) {
|
||||
return
|
||||
}
|
||||
|
||||
stripLeading = true
|
||||
void terminalApi.write(id, '\f')
|
||||
term.clearSelection()
|
||||
}, 120)
|
||||
}
|
||||
|
||||
cleanup.push(() => {
|
||||
if (gapCleanupTimer) {
|
||||
window.clearTimeout(gapCleanupTimer)
|
||||
}
|
||||
})
|
||||
|
||||
const fitAndResize = () => {
|
||||
if (disposed || !host.isConnected || host.clientWidth <= 0 || host.clientHeight <= 0) {
|
||||
return
|
||||
|
|
@ -504,7 +474,6 @@ export function useTerminalSession({ id, cwd, active, onAddSelectionToChat, onSh
|
|||
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 })
|
||||
scheduleGapCleanup()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -543,12 +512,6 @@ export function useTerminalSession({ id, cwd, active, onAddSelectionToChat, onSh
|
|||
const id = sessionIdRef.current
|
||||
|
||||
if (id) {
|
||||
// Once the user submits a line, real output may follow — stop the
|
||||
// pristine-prompt gap cleanup so we never clear command scrollback.
|
||||
if (promptPristine && data.includes('\r')) {
|
||||
promptPristine = false
|
||||
}
|
||||
|
||||
void terminalApi.write(id, data)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1887,6 +1887,7 @@ export const en: Translations = {
|
|||
terminalsAria: 'Terminals',
|
||||
terminalNew: 'New terminal',
|
||||
terminalCloseOthers: 'Close others',
|
||||
terminalCloseAll: 'Close all',
|
||||
addToChat: 'Add to chat'
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -2004,6 +2004,10 @@ export const ja = defineLocale({
|
|||
loadingTree: 'ファイルツリーを読み込み中',
|
||||
loadingFiles: 'ファイルを読み込み中',
|
||||
terminalHide: 'ターミナルを非表示',
|
||||
terminalsAria: 'ターミナル',
|
||||
terminalNew: '新しいターミナル',
|
||||
terminalCloseOthers: '他を閉じる',
|
||||
terminalCloseAll: 'すべて閉じる',
|
||||
addToChat: 'チャットに追加'
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1540,6 +1540,7 @@ export interface Translations {
|
|||
terminalsAria: string
|
||||
terminalNew: string
|
||||
terminalCloseOthers: string
|
||||
terminalCloseAll: string
|
||||
addToChat: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1942,6 +1942,10 @@ export const zhHant = defineLocale({
|
|||
loadingTree: '正在載入檔案樹',
|
||||
loadingFiles: '正在載入檔案',
|
||||
terminalHide: '隱藏終端機',
|
||||
terminalsAria: '終端機',
|
||||
terminalNew: '新增終端機',
|
||||
terminalCloseOthers: '關閉其他',
|
||||
terminalCloseAll: '全部關閉',
|
||||
addToChat: '新增至聊天'
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -2057,6 +2057,7 @@ export const zh: Translations = {
|
|||
terminalsAria: '终端',
|
||||
terminalNew: '新建终端',
|
||||
terminalCloseOthers: '关闭其他',
|
||||
terminalCloseAll: '关闭全部',
|
||||
addToChat: '添加到对话'
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue