Skip to content
6 changes: 3 additions & 3 deletions app/main/handlers/handleStreamWithContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ module.exports = {

streamMap.set(token, stream)
stream.on('data', (data) => {
if (!windows) {
if (!windows || windows.isDestroyed() || windows.webContents.isDestroyed()) {
return
}
windows.webContents.send(`${token}-data`, data)
})
stream.on('error', (error) => {
if (!windows) {
if (!windows || windows.isDestroyed() || windows.webContents.isDestroyed()) {
return
}
windows.webContents.send(`${token}-error`, error && error.details)
})
stream.on('end', () => {
streamMap.delete(token)
if (!windows) {
if (!windows || windows.isDestroyed() || windows.webContents.isDestroyed()) {
return
}
windows.webContents.send(`${token}-end`)
Expand Down
16 changes: 8 additions & 8 deletions app/main/handlers/yakRunnerTerminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ module.exports = (win, getClient) => {
// 如果有问题,重置
stream.on('error', (e) => {
removeStreamRunner(id)
if (win) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) {
win.webContents.send('client-listening-terminal-error-yakRunner', { id, path })
}
})

// 发送回数据
stream.on('data', (data) => {
if (data.control) {
if (win && data.waiting) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed() && data.waiting) {
win.webContents.send(`client-listening-terminal-success-yakRunner`, { id, path, result: data })
}
if (win && data.closed) {
Expand All @@ -72,13 +72,13 @@ module.exports = (win, getClient) => {
return
}

if (win) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) {
win.webContents.send(`client-listening-terminal-data-yakRunner`, { id, path, result: data })
}
})
stream.on('end', () => {
removeStreamRunner(id)
if (win) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) {
win.webContents.send('client-listening-terminal-end-yakRunner', { id, path })
}
})
Expand All @@ -95,15 +95,15 @@ module.exports = (win, getClient) => {
// 如果有问题,重置
stream.on('error', (e) => {
removeStreamRunner(id)
if (win) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) {
win.webContents.send('client-listening-terminal-error-aiAgent', { id, path })
}
})

// 发送回数据
stream.on('data', (data) => {
if (data.control) {
if (win && data.waiting) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed() && data.waiting) {
win.webContents.send(`client-listening-terminal-success-aiAgent`, { id, path, result: data })
}
if (win && data.closed) {
Expand All @@ -112,13 +112,13 @@ module.exports = (win, getClient) => {
return
}

if (win) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) {
win.webContents.send(`client-listening-terminal-data-aiAgent`, { id, path, result: data })
}
})
stream.on('end', () => {
removeStreamRunner(id)
if (win) {
if (win && !win.isDestroyed() && !win.webContents.isDestroyed()) {
win.webContents.send('client-listening-terminal-end-aiAgent', { id, path })
}
})
Expand Down
10 changes: 6 additions & 4 deletions app/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,16 @@ let readyWinShow = false
function createWindow() {
const minWidth = 900
const minHeight = 650
const defaultWidth = 1400
const defaultHeight = 880
const state = windowStateKeeper({
defaultWidth: minWidth,
defaultHeight: minHeight,
defaultWidth: defaultWidth,
defaultHeight: defaultHeight,
path: getWindowStatePath(),
file: 'yakit-window-state.json',
})
const width = Math.max(state.width ?? minWidth, minWidth)
const height = Math.max(state.height ?? minHeight, minHeight)
const width = Math.max(state.width ?? defaultWidth, minWidth)
const height = Math.max(state.height ?? defaultHeight, minHeight)

win = new BrowserWindow({
x: state.x,
Expand Down
33 changes: 29 additions & 4 deletions app/renderer/engine-link-startup/src/utils/xtermUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,38 @@ export const writeExecResultXTerm = (xterm: any, result: ExecResult, encoding?:
}

export const xtermFit = (xtermRef: any, columns?: number, rows?: number) => {
if (xtermRef && xtermRef?.current && xtermRef.current.terminal) {
xtermRef.current.terminal.resize(columns || 100, rows || 10)
if (xtermRef && xtermRef?.current && isTerminalReady(xtermRef.current.terminal)) {
try {
xtermRef.current.terminal.resize(columns || 100, rows || 10)
} catch (e) {
// 关键词: xterm resize error swallowed, 防止 _renderService.dimensions 崩溃冒泡到 UI
}
}
}

export const xtermClear = (xtermRef: any, columns?: number, rows?: number) => {
if (xtermRef && xtermRef?.current && xtermRef.current.terminal) {
xtermRef.current.terminal.reset()
if (xtermRef && xtermRef?.current && isTerminalReady(xtermRef.current.terminal)) {
try {
xtermRef.current.terminal.reset()
} catch (e) {
// 关键词: xterm reset error swallowed, 防止 Viewport.syncScrollArea 在 renderer 未就绪时崩溃
}
}
}

// 关键词: xterm dimensions crash, _renderService undefined, terminal.reset 报错
// xterm 的 RenderService.dimensions getter 在 renderer 未就绪/已 dispose 时会抛
// "Cannot read properties of undefined (reading 'dimensions')"。
// 该校验用于在 open 尚未完成、或组件已被 dispose 时,避免 reset/resize 触发 Viewport.syncScrollArea 崩溃。
export const isTerminalReady = (terminal: any): boolean => {
if (!terminal) return false
// terminal.element 仅在 Terminal.open 成功后被赋值
if (!terminal.element) return false
// 兜底:能拿到内部 RenderService 时,用其公开的 hasRenderer 判定
const renderService = terminal._core?._renderService
if (renderService && typeof renderService.hasRenderer === 'function') {
return renderService.hasRenderer()
}
// 拿不到内部结构时,退化成仅依赖 element 是否存在
return true
}
4 changes: 2 additions & 2 deletions app/renderer/src/main/public/locales/zh/aiAgent.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@
"getGlobalKnowledgeBaseFailed": "获取全局知识库失败: {{error}}"
},
"AIAgentTabs": {
"historyChat": "历史会话",
"historyChat": "历史",
"config": "配置",
"skill": "技能",
"tool": "工具"
Expand Down Expand Up @@ -432,7 +432,7 @@
"focusModeTooltip": "复杂场景限定使用框架,限制AI思考范围"
},
"HistoryChat": {
"title": "历史会话",
"title": "历史",
"noChatsToClear": "暂无可清除的会话",
"allChatsCleared": "已清空全部会话",
"clearFailed": "清除会话失败:{{error}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import useListenWidth from '@/pages/pluginHub/hooks/useListenWidth'
import { System, SystemInfo, handleFetchSystem } from '@/constants/hardware'
import { getClipboardText, setClipboardText } from '@/utils/clipboard'
import { useXTermOptions } from '@/hook/useXTermOptions/useXTermOptions'
import { useCampare } from '@/hook/useCompare/useCompare'
import { useI18nNamespaces } from '@/i18n/useI18nNamespaces'
import { isTerminalReady } from '@/utils/xtermUtils'

export interface YakitXtermRefProps {
terminal: Terminal
Expand Down Expand Up @@ -182,6 +182,15 @@ const YakitXterm: React.FC<IProps> = forwardRef((props, ref) => {
getTerminal: () => terminalRef.current,
})

const safeFit = useMemoizedFn(() => {
if (!fitAddonRef.current || !isTerminalReady(terminalRef.current)) return
try {
fitAddonRef.current.fit()
} catch (e) {
// 关键词: xterm fit error swallowed, 防止 _renderService.dimensions 崩溃冒泡到 UI
}
})

useEffect(() => {
if (!systemRef.current) {
handleFetchSystem(() => (systemRef.current = SystemInfo.system))
Expand Down Expand Up @@ -214,16 +223,29 @@ const YakitXterm: React.FC<IProps> = forwardRef((props, ref) => {
if (terminalRef.current && terminalDivRef.current) {
// Creates the terminal within the container element.
terminalRef.current.open(terminalDivRef.current)
fitAddonRef.current.fit()
// 关键词: xterm open 后 fit 延后一帧, 避免 RenderService.dimensions 未就绪崩溃
// Terminal.open 内部 Viewport 构造会异步(100ms 内)首次 syncScrollArea,
// 同步立刻 fit() 在某些时序下会触发 _renderService.dimensions 抛错。
const rafId = requestAnimationFrame(() => {
safeFit()
})
return () => {
cancelAnimationFrame(rafId)
terminalRef.current.dispose()
fitAddonRef.current.dispose()
}
}
return () => {
terminalRef.current.dispose()
fitAddonRef.current.dispose()
}
}, [])
useEffect(() => {
fitAddonRef.current.fit()
}, [terminalDivHeight, terminalDivWidth])
const rafId = requestAnimationFrame(() => {
safeFit()
})
return () => cancelAnimationFrame(rafId)
}, [terminalDivHeight, terminalDivWidth, safeFit])
const customKeyEventHandler = (e: KeyboardEvent) => {
if (props.customKeyEventHandler) {
return props.customKeyEventHandler(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
// }
// }
.task-loading-footer {
padding: 0 12px;
padding: 0 12px 60px;
}
}

Expand Down
Loading
Loading