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:
1 parent
e6fc021903
commit
6c100542c5
11 files changed
+130
-19
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user