diff --git a/apps/desktop/src/app/right-sidebar/terminal/rail.tsx b/apps/desktop/src/app/right-sidebar/terminal/rail.tsx
index 8a9c74cb511..c5a07bb8e6d 100644
--- a/apps/desktop/src/app/right-sidebar/terminal/rail.tsx
+++ b/apps/desktop/src/app/right-sidebar/terminal/rail.tsx
@@ -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
closeOtherTerminals(term.id)}>
{t.rightSidebar.terminalCloseOthers}
+ {t.rightSidebar.terminalCloseAll}
setTerminalTakeover(false)}>{t.rightSidebar.terminalHide}
diff --git a/apps/desktop/src/app/right-sidebar/terminal/terminals.ts b/apps/desktop/src/app/right-sidebar/terminal/terminals.ts
index f008780a5ce..1e7d36240da 100644
--- a/apps/desktop/src/app/right-sidebar/terminal/terminals.ts
+++ b/apps/desktop/src/app/right-sidebar/terminal/terminals.ts
@@ -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
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 b9f349dc7b2..9a7e690c7c8 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
@@ -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)
}
})
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index 74df4c8f0b4..8e9243bb259 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -1887,6 +1887,7 @@ export const en: Translations = {
terminalsAria: 'Terminals',
terminalNew: 'New terminal',
terminalCloseOthers: 'Close others',
+ terminalCloseAll: 'Close all',
addToChat: 'Add to chat'
},
diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts
index bdf4e882929..d3b29ce9f2c 100644
--- a/apps/desktop/src/i18n/ja.ts
+++ b/apps/desktop/src/i18n/ja.ts
@@ -2004,6 +2004,10 @@ export const ja = defineLocale({
loadingTree: 'ファイルツリーを読み込み中',
loadingFiles: 'ファイルを読み込み中',
terminalHide: 'ターミナルを非表示',
+ terminalsAria: 'ターミナル',
+ terminalNew: '新しいターミナル',
+ terminalCloseOthers: '他を閉じる',
+ terminalCloseAll: 'すべて閉じる',
addToChat: 'チャットに追加'
},
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index aace3e5e9ab..31a26dc187a 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -1540,6 +1540,7 @@ export interface Translations {
terminalsAria: string
terminalNew: string
terminalCloseOthers: string
+ terminalCloseAll: string
addToChat: string
}
diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts
index 24e0ff12dac..6fc1c64203f 100644
--- a/apps/desktop/src/i18n/zh-hant.ts
+++ b/apps/desktop/src/i18n/zh-hant.ts
@@ -1942,6 +1942,10 @@ export const zhHant = defineLocale({
loadingTree: '正在載入檔案樹',
loadingFiles: '正在載入檔案',
terminalHide: '隱藏終端機',
+ terminalsAria: '終端機',
+ terminalNew: '新增終端機',
+ terminalCloseOthers: '關閉其他',
+ terminalCloseAll: '全部關閉',
addToChat: '新增至聊天'
},
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index 4564e746139..7d7cd5d5016 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -2057,6 +2057,7 @@ export const zh: Translations = {
terminalsAria: '终端',
terminalNew: '新建终端',
terminalCloseOthers: '关闭其他',
+ terminalCloseAll: '关闭全部',
addToChat: '添加到对话'
},