fix: batch of review findings — dead install button, history pollution, TUI completion interference, ssh split session kill, scrollback live apply, zmodem second transfer, sftp shell quoting, replay buffer leak, release guards

This commit is contained in:
Bill committed 2026-09-14 02:20:40 +08:00
1 parent e6fc021903
commit 6c100542c5
11 files changed
+130 -19

No files matched your search

+1 -1
View File
@@ -89,7 +89,7 @@ if (typeof window !== 'undefined' && !window.api) {
deleteLayout: async () => undefined,
updateCheck: async () => ({ status: 'dev' as const, currentVersion: 'dev' }),
updateDownload: async () => undefined,
updateInstall: noop,
updateInstall: async () => undefined,
updateChangelog: async () => [],
onUpdateState: () => noop
}
+1 -1
View File
@@ -67,7 +67,7 @@ export function AboutTab(): React.JSX.Element {
</Button>
)}
{status === 'downloaded' && (
<Button type="primary" onClick={() => window.api.updateInstall()}>
<Button type="primary" onClick={() => void window.api.updateInstall()}>
重启安装
</Button>
)}
+24 -2
View File
@@ -154,7 +154,7 @@ function stepLineBuffer(
// clear-line) only removes buffer content; never appends.
const first = data[0]
if (b === '' && first === '\x1b') return { line: '' }
if (first === '\r') return submitOr(b)
if (first === '\r') return submitOr(outBuf, b)
if (first === '\x7f') {
outBuf.current = b.slice(0, -1)
return { line: outBuf.current }
@@ -168,8 +168,12 @@ function stepLineBuffer(
return { line: outBuf.current }
}
function submitOr(b: string): { line: string; submit?: string } {
function submitOr(outBuf: React.MutableRefObject<string>, b: string): { line: string; submit?: string } {
const t = b.trim()
// Commit the line out of the buffer: the shell has it now, and anything typed
// next belongs to a fresh line. Leaving it in made every following command
// accumulate onto the previous one (`ls` + `pwd` → `lspwd` in the history).
outBuf.current = ''
return t ? { line: '', submit: t } : { line: '', submit: '' }
}
@@ -684,6 +688,21 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
// now so this redraw chunk replaces (not appends to) the line buffer.
const consumeRewrite = diffRewriteRef.current
diffRewriteRef.current = false
// Full-screen TUIs (vim, Claude Code, …) own the alternate buffer, their
// own key handling and their own cursor: the line buffer, the command
// history and the completion popup have no business there. Interfering
// swallowed ↑/↓/Tab/Esc (Esc could not even leave vim's insert mode) and
// wrote TUI keystrokes into the command history.
if (term.buffer.active.type === 'alternate') {
lineBufRef.current = ''
if (suggestionsRef.current.length > 0) {
suggestionsRef.current = []
selIndexRef.current = 0
setSuggestions([])
setSuggestionIndex(0)
}
return
}
// M5: line capture → recordCommand + inline completion overlay.
const step = stepLineBuffer(lineBufRef, data, consumeRewrite)
if (typeof step.submit === 'string' && step.submit) {
@@ -810,6 +829,9 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
term.options.cursorBlink = tSettings.cursorBlink
term.options.cursorStyle = tSettings.cursorStyle
term.options.cursorInactiveStyle = tSettings.cursorInactiveStyle
// Live-appliable in xterm 6: without this the setting only took effect for
// terminals opened after the change, which reads as "the setting is broken".
term.options.scrollback = tSettings.scrollback
term.options.theme = withChromeColors(getThemeById(tSettings.themeId, settings.customThemes).colors as ITheme)
scheduleFit()
}, [tSettings, settings.customThemes, scheduleFit])
+31 -2
View File
@@ -243,9 +243,35 @@ export default function Workspace({ onOpenSettings }: WorkspaceProps): React.JSX
if (mode === 'terminal') terminalApiRef.current = api
else sshApiRef.current = api
// One pty/ssh session can back several panels — an SSH split mirrors a
// single session on purpose. So closing a panel must not kill a session
// another panel is still showing; count the panels per session (seeding
// from the panels already present, e.g. a restored layout) and kill only
// when the last one goes away.
const useCount = new Map<string, number>()
for (const existing of api.panels) {
const sid = sessionIdOf(existing)
if (sid) useCount.set(sid, (useCount.get(sid) ?? 0) + 1)
}
const countPanel = (panel: IDockviewPanel): void => {
const sid = sessionIdOf(panel)
if (sid) useCount.set(sid, (useCount.get(sid) ?? 0) + 1)
}
const releaseSession = (panel: IDockviewPanel): void => {
const sid = sessionIdOf(panel)
if (!sid) return
const left = (useCount.get(sid) ?? 1) - 1
if (left > 0) {
useCount.set(sid, left)
return
}
useCount.delete(sid)
killSession(sid)
}
const cleanups = [
api.onDidRemovePanel((panel: IDockviewPanel) => {
killSession(sessionIdOf(panel))
releaseSession(panel)
recomputeLocalPanels()
recountAll()
}),
@@ -261,7 +287,10 @@ export default function Workspace({ onOpenSettings }: WorkspaceProps): React.JSX
recomputeLocalPanels()
recountAll()
}),
api.onDidAddPanel(() => recountAll())
api.onDidAddPanel((panel: IDockviewPanel) => {
countPanel(panel)
recountAll()
})
]
disposablesRef.current.push(...cleanups)
},