feat(workspace): preserve terminal sessions across view changes
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
"version": "1.1.19",
|
"version": "1.1.20",
|
||||||
"description": "Autonomy Studio LMS Electron app",
|
"description": "Autonomy Studio LMS Electron app",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
+15
-6
@@ -977,7 +977,7 @@ function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record<string
|
|||||||
" maintainer_email='student@autonomy-studio.local',",
|
" maintainer_email='student@autonomy-studio.local',",
|
||||||
` description='${module.name}',`,
|
` description='${module.name}',`,
|
||||||
" license='Apache-2.0',",
|
" license='Apache-2.0',",
|
||||||
` entry_points={{'console_scripts': ['${executable} = ${module.packageName}.node:main']}},`,
|
` entry_points={'console_scripts': ['${executable} = ${module.packageName}.node:main']},`,
|
||||||
')',
|
')',
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
@@ -4287,7 +4287,13 @@ function buildNav2DockerBuildScript(): string {
|
|||||||
'fi',
|
'fi',
|
||||||
'',
|
'',
|
||||||
'MODULE_PATH="$MODULE_ROOT/$MODULE_FOLDER"',
|
'MODULE_PATH="$MODULE_ROOT/$MODULE_FOLDER"',
|
||||||
'test -d "$MODULE_PATH/src" || { echo "Module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
|
'if [[ "$MODULE_FOLDER" == *lms-nav2-custom* ]]; then',
|
||||||
|
' test -f "$MODULE_PATH/package.xml" || { echo "Custom module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
|
||||||
|
' SOURCE_BASE_PATH=.',
|
||||||
|
'else',
|
||||||
|
' test -d "$MODULE_PATH/src" || { echo "Module source is missing: $MODULE_FOLDER" >&2; exit 1; }',
|
||||||
|
' SOURCE_BASE_PATH=src',
|
||||||
|
'fi',
|
||||||
'BUILD_CONTAINER="nav2-build-${TARGET}"',
|
'BUILD_CONTAINER="nav2-build-${TARGET}"',
|
||||||
'cleanup() {',
|
'cleanup() {',
|
||||||
' docker run --rm -v "$MODULE_ROOT:/modules" "$IMAGE" bash -lc "chown -R $(id -u):$(id -g) \'/modules/$MODULE_FOLDER/build\' \'/modules/$MODULE_FOLDER/install\' \'/modules/$MODULE_FOLDER/log\' 2>/dev/null || true" >/dev/null 2>&1 || true',
|
' docker run --rm -v "$MODULE_ROOT:/modules" "$IMAGE" bash -lc "chown -R $(id -u):$(id -g) \'/modules/$MODULE_FOLDER/build\' \'/modules/$MODULE_FOLDER/install\' \'/modules/$MODULE_FOLDER/log\' 2>/dev/null || true" >/dev/null 2>&1 || true',
|
||||||
@@ -4302,10 +4308,13 @@ function buildNav2DockerBuildScript(): string {
|
|||||||
" \"$IMAGE\" bash -lc '",
|
" \"$IMAGE\" bash -lc '",
|
||||||
' set -e',
|
' set -e',
|
||||||
' source /opt/ros/humble/setup.bash',
|
' source /opt/ros/humble/setup.bash',
|
||||||
' mapfile -t paths < <(colcon list --base-paths src --paths-only)',
|
' mapfile -t paths < <(colcon list --base-paths "$3" --paths-only)',
|
||||||
' ROSDEP_SKIP_ARGS=()',
|
' ROSDEP_SKIP_ARGS=()',
|
||||||
|
' if [[ "$1" == *lms-nav2-custom* ]]; then',
|
||||||
|
' ROSDEP_SKIP_ARGS+=(--skip-keys ament_python)',
|
||||||
|
' fi',
|
||||||
' if ! apt-cache show ros-humble-gazebo-ros >/dev/null 2>&1; then',
|
' if ! apt-cache show ros-humble-gazebo-ros >/dev/null 2>&1; then',
|
||||||
' ROSDEP_SKIP_ARGS=(--skip-keys gazebo_ros)',
|
' ROSDEP_SKIP_ARGS+=(--skip-keys gazebo_ros)',
|
||||||
' fi',
|
' fi',
|
||||||
" rosdep install --from-paths \"${paths[@]}\" --ignore-src \"${ROSDEP_SKIP_ARGS[@]}\" \\",
|
" rosdep install --from-paths \"${paths[@]}\" --ignore-src \"${ROSDEP_SKIP_ARGS[@]}\" \\",
|
||||||
" --dependency-types buildtool --dependency-types build \\",
|
" --dependency-types buildtool --dependency-types build \\",
|
||||||
@@ -4314,9 +4323,9 @@ function buildNav2DockerBuildScript(): string {
|
|||||||
' if [[ "$1" == *lms-nav2-gazebo-slam* && "$1" != *lms-nav2-gazebo-slam3d* ]]; then',
|
' if [[ "$1" == *lms-nav2-gazebo-slam* && "$1" != *lms-nav2-gazebo-slam3d* ]]; then',
|
||||||
' BUILD_TARGETS=(slam_pkg slam2d_pkg slam3d_pkg)',
|
' BUILD_TARGETS=(slam_pkg slam2d_pkg slam3d_pkg)',
|
||||||
' fi',
|
' fi',
|
||||||
" colcon --log-base log build --base-paths src --build-base build --install-base install \\",
|
" colcon --log-base log build --base-paths \"$3\" --build-base build --install-base install \\",
|
||||||
' --executor sequential --cmake-args -DBUILD_TESTING=OFF --packages-up-to "${BUILD_TARGETS[@]}"',
|
' --executor sequential --cmake-args -DBUILD_TESTING=OFF --packages-up-to "${BUILD_TARGETS[@]}"',
|
||||||
"' bash \"$MODULE_FOLDER\" \"$TARGET\"",
|
"' bash \"$MODULE_FOLDER\" \"$TARGET\" \"$SOURCE_BASE_PATH\"",
|
||||||
''
|
''
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2081,10 +2081,12 @@ function LocalPcWorkspaceMockup({
|
|||||||
fitTerminal,
|
fitTerminal,
|
||||||
copyTerminalSelection,
|
copyTerminalSelection,
|
||||||
pasteTerminalClipboard,
|
pasteTerminalClipboard,
|
||||||
startTerminalResize
|
startTerminalResize,
|
||||||
|
stopAllTerminalSessions
|
||||||
} = useWorkspaceTerminal({
|
} = useWorkspaceTerminal({
|
||||||
moduleName: selectedModule?.name,
|
moduleName: selectedModule?.name,
|
||||||
moduleFolderName: selectedModule?.folderName,
|
moduleFolderName: selectedModule?.folderName,
|
||||||
|
openModuleNames: moduleTabs.map((module) => module.name),
|
||||||
clonePathPreview,
|
clonePathPreview,
|
||||||
remoteWorkspace,
|
remoteWorkspace,
|
||||||
getRemoteWorkspaceTarget,
|
getRemoteWorkspaceTarget,
|
||||||
@@ -2950,6 +2952,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
setPendingEditorCloseRequest({ kind: 'workspace' })
|
setPendingEditorCloseRequest({ kind: 'workspace' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
stopAllTerminalSessions()
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
const performPendingEditorCloseRequest = (): void => {
|
const performPendingEditorCloseRequest = (): void => {
|
||||||
@@ -2995,6 +2998,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
discardDirtyFiles(Object.keys(dirtyEditorFiles).filter((file) => dirtyEditorFiles[file]))
|
discardDirtyFiles(Object.keys(dirtyEditorFiles).filter((file) => dirtyEditorFiles[file]))
|
||||||
|
stopAllTerminalSessions()
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
const revealSearchMatch = (lineNumber: number, query: string): boolean => {
|
const revealSearchMatch = (lineNumber: number, query: string): boolean => {
|
||||||
|
|||||||
@@ -10,9 +10,19 @@ type TerminalContextMenu = {
|
|||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ManagedTerminalSession = {
|
||||||
|
moduleName: string
|
||||||
|
remoteWorkspace: boolean
|
||||||
|
attachedTerminal: XTerm | null
|
||||||
|
suppressedData: string[]
|
||||||
|
removeDataListener: () => void
|
||||||
|
removeSuppressListener: () => void
|
||||||
|
}
|
||||||
|
|
||||||
type UseWorkspaceTerminalOptions = {
|
type UseWorkspaceTerminalOptions = {
|
||||||
moduleName?: string | null
|
moduleName?: string | null
|
||||||
moduleFolderName?: string | null
|
moduleFolderName?: string | null
|
||||||
|
openModuleNames: string[]
|
||||||
clonePathPreview: string
|
clonePathPreview: string
|
||||||
remoteWorkspace: boolean
|
remoteWorkspace: boolean
|
||||||
getRemoteWorkspaceTarget: () => RemoteWorkspaceTarget
|
getRemoteWorkspaceTarget: () => RemoteWorkspaceTarget
|
||||||
@@ -32,11 +42,13 @@ type UseWorkspaceTerminalResult = {
|
|||||||
copyTerminalSelection: () => void
|
copyTerminalSelection: () => void
|
||||||
pasteTerminalClipboard: () => void
|
pasteTerminalClipboard: () => void
|
||||||
startTerminalResize: (event: React.MouseEvent<HTMLDivElement>) => void
|
startTerminalResize: (event: React.MouseEvent<HTMLDivElement>) => void
|
||||||
|
stopAllTerminalSessions: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkspaceTerminal({
|
export function useWorkspaceTerminal({
|
||||||
moduleName,
|
moduleName,
|
||||||
moduleFolderName,
|
moduleFolderName,
|
||||||
|
openModuleNames,
|
||||||
clonePathPreview,
|
clonePathPreview,
|
||||||
remoteWorkspace,
|
remoteWorkspace,
|
||||||
getRemoteWorkspaceTarget,
|
getRemoteWorkspaceTarget,
|
||||||
@@ -52,12 +64,31 @@ export function useWorkspaceTerminal({
|
|||||||
const terminalFitRef = useRef<FitAddon | null>(null)
|
const terminalFitRef = useRef<FitAddon | null>(null)
|
||||||
const terminalSessionIdRef = useRef<string | null>(null)
|
const terminalSessionIdRef = useRef<string | null>(null)
|
||||||
const remoteTargetResolverRef = useRef(getRemoteWorkspaceTarget)
|
const remoteTargetResolverRef = useRef(getRemoteWorkspaceTarget)
|
||||||
const terminalSuppressQueueRef = useRef<string[]>([])
|
|
||||||
const terminalOutputBufferRef = useRef<Record<string, string>>({})
|
const terminalOutputBufferRef = useRef<Record<string, string>>({})
|
||||||
|
const terminalSessionsRef = useRef<Map<string, ManagedTerminalSession>>(new Map())
|
||||||
|
const resetKeyRef = useRef(resetKey)
|
||||||
const [terminalPanelHeight, setTerminalPanelHeight] = useState(235)
|
const [terminalPanelHeight, setTerminalPanelHeight] = useState(235)
|
||||||
|
|
||||||
remoteTargetResolverRef.current = getRemoteWorkspaceTarget
|
remoteTargetResolverRef.current = getRemoteWorkspaceTarget
|
||||||
|
|
||||||
|
const stopTerminalSession = (sessionId: string): void => {
|
||||||
|
const session = terminalSessionsRef.current.get(sessionId)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
session.attachedTerminal = null
|
||||||
|
session.removeDataListener()
|
||||||
|
session.removeSuppressListener()
|
||||||
|
terminalSessionsRef.current.delete(sessionId)
|
||||||
|
delete terminalOutputBufferRef.current[sessionId]
|
||||||
|
|
||||||
|
if (session.remoteWorkspace) window.remoteWorkspace.terminal.stop(sessionId)
|
||||||
|
else window.localWorkspace.terminal.stop(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAllTerminalSessions = (): void => {
|
||||||
|
Array.from(terminalSessionsRef.current.keys()).forEach(stopTerminalSession)
|
||||||
|
}
|
||||||
|
|
||||||
const fitTerminal = (): void => {
|
const fitTerminal = (): void => {
|
||||||
terminalFitRef.current?.fit()
|
terminalFitRef.current?.fit()
|
||||||
}
|
}
|
||||||
@@ -115,6 +146,21 @@ export function useWorkspaceTerminal({
|
|||||||
document.addEventListener('mouseup', onMouseUp)
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openModuleNamesKey = openModuleNames.join('\u0000')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const openModuleNameSet = new Set(openModuleNames)
|
||||||
|
Array.from(terminalSessionsRef.current.entries()).forEach(([sessionId, session]) => {
|
||||||
|
if (!openModuleNameSet.has(session.moduleName)) stopTerminalSession(sessionId)
|
||||||
|
})
|
||||||
|
}, [openModuleNamesKey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resetKeyRef.current === resetKey) return
|
||||||
|
resetKeyRef.current = resetKey
|
||||||
|
stopAllTerminalSessions()
|
||||||
|
}, [resetKey])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!moduleName || !terminalActive || !terminalHostRef.current || terminalRef.current) return
|
if (!moduleName || !terminalActive || !terminalHostRef.current || terminalRef.current) return
|
||||||
const terminalSessionId = getModuleTerminalSessionId(moduleName)
|
const terminalSessionId = getModuleTerminalSessionId(moduleName)
|
||||||
@@ -164,22 +210,36 @@ export function useWorkspaceTerminal({
|
|||||||
terminalFitRef.current = fitAddon
|
terminalFitRef.current = fitAddon
|
||||||
|
|
||||||
const terminalApi = remoteWorkspace ? window.remoteWorkspace.terminal : window.localWorkspace.terminal
|
const terminalApi = remoteWorkspace ? window.remoteWorkspace.terminal : window.localWorkspace.terminal
|
||||||
const removeDataListener = terminalApi.onData((data) => {
|
let terminalSession = terminalSessionsRef.current.get(terminalSessionId)
|
||||||
const suppressQueue = terminalSuppressQueueRef.current
|
if (!terminalSession || terminalSession.remoteWorkspace !== remoteWorkspace) {
|
||||||
if (suppressQueue.length && suppressQueue[0] === data) {
|
if (terminalSession) stopTerminalSession(terminalSessionId)
|
||||||
suppressQueue.shift()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextData = stripInternalTerminalCommands(data)
|
const suppressedData: string[] = []
|
||||||
if (nextData) {
|
const removeDataListener = terminalApi.onData((data) => {
|
||||||
|
if (suppressedData.length && suppressedData[0] === data) {
|
||||||
|
suppressedData.shift()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextData = stripInternalTerminalCommands(data)
|
||||||
|
if (!nextData) return
|
||||||
terminalOutputBufferRef.current[terminalSessionId] = `${terminalOutputBufferRef.current[terminalSessionId] ?? ''}${nextData}`.slice(-60000)
|
terminalOutputBufferRef.current[terminalSessionId] = `${terminalOutputBufferRef.current[terminalSessionId] ?? ''}${nextData}`.slice(-60000)
|
||||||
term.write(nextData)
|
terminalSessionsRef.current.get(terminalSessionId)?.attachedTerminal?.write(nextData)
|
||||||
|
}, terminalSessionId)
|
||||||
|
const removeSuppressListener = terminalApi.onSuppressData((data) => {
|
||||||
|
suppressedData.push(data)
|
||||||
|
}, terminalSessionId)
|
||||||
|
terminalSession = {
|
||||||
|
moduleName,
|
||||||
|
remoteWorkspace,
|
||||||
|
attachedTerminal: null,
|
||||||
|
suppressedData,
|
||||||
|
removeDataListener,
|
||||||
|
removeSuppressListener
|
||||||
}
|
}
|
||||||
}, terminalSessionId)
|
terminalSessionsRef.current.set(terminalSessionId, terminalSession)
|
||||||
const removeSuppressListener = terminalApi.onSuppressData((data) => {
|
}
|
||||||
terminalSuppressQueueRef.current.push(data)
|
terminalSession.attachedTerminal = term
|
||||||
}, terminalSessionId)
|
|
||||||
const inputDisposable = term.onData((data) => terminalApi.write(data, terminalSessionId))
|
const inputDisposable = term.onData((data) => terminalApi.write(data, terminalSessionId))
|
||||||
|
|
||||||
let sizeSyncTimer: number | null = null
|
let sizeSyncTimer: number | null = null
|
||||||
@@ -215,9 +275,10 @@ export function useWorkspaceTerminal({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
void startTerminal.then((started) => {
|
void startTerminal.then((started) => {
|
||||||
|
if (disposed || !started) return
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
if (!started) return
|
|
||||||
sizeSyncTimer = window.setTimeout(() => {
|
sizeSyncTimer = window.setTimeout(() => {
|
||||||
|
if (disposed) return
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
if (remoteWorkspace) {
|
if (remoteWorkspace) {
|
||||||
void window.remoteWorkspace.terminal.resize(remoteTargetResolverRef.current(), term.cols, term.rows, terminalSessionId)
|
void window.remoteWorkspace.terminal.resize(remoteTargetResolverRef.current(), term.cols, term.rows, terminalSessionId)
|
||||||
@@ -237,19 +298,14 @@ export function useWorkspaceTerminal({
|
|||||||
window.removeEventListener('resize', onResize)
|
window.removeEventListener('resize', onResize)
|
||||||
terminalElement.removeEventListener('contextmenu', handleTerminalContextMenu)
|
terminalElement.removeEventListener('contextmenu', handleTerminalContextMenu)
|
||||||
terminalElement.removeEventListener('mousedown', focusTerminal)
|
terminalElement.removeEventListener('mousedown', focusTerminal)
|
||||||
removeDataListener()
|
const currentSession = terminalSessionsRef.current.get(terminalSessionId)
|
||||||
removeSuppressListener()
|
if (currentSession?.attachedTerminal === term) currentSession.attachedTerminal = null
|
||||||
inputDisposable.dispose()
|
inputDisposable.dispose()
|
||||||
if (sizeSyncTimer !== null) window.clearTimeout(sizeSyncTimer)
|
if (sizeSyncTimer !== null) window.clearTimeout(sizeSyncTimer)
|
||||||
// Full-screen programs such as vi cannot be restored from a partial raw output buffer.
|
|
||||||
delete terminalOutputBufferRef.current[terminalSessionId]
|
|
||||||
if (remoteWorkspace) window.remoteWorkspace.terminal.stop(terminalSessionId)
|
|
||||||
else window.localWorkspace.terminal.stop(terminalSessionId)
|
|
||||||
term.dispose()
|
term.dispose()
|
||||||
terminalRef.current = null
|
terminalRef.current = null
|
||||||
terminalFitRef.current = null
|
terminalFitRef.current = null
|
||||||
terminalSessionIdRef.current = null
|
terminalSessionIdRef.current = null
|
||||||
terminalSuppressQueueRef.current = []
|
|
||||||
}
|
}
|
||||||
}, [clonePathPreview, dark, moduleFolderName, moduleName, remoteWorkspace, resetKey, terminalActive])
|
}, [clonePathPreview, dark, moduleFolderName, moduleName, remoteWorkspace, resetKey, terminalActive])
|
||||||
|
|
||||||
@@ -262,6 +318,7 @@ export function useWorkspaceTerminal({
|
|||||||
fitTerminal,
|
fitTerminal,
|
||||||
copyTerminalSelection,
|
copyTerminalSelection,
|
||||||
pasteTerminalClipboard,
|
pasteTerminalClipboard,
|
||||||
startTerminalResize
|
startTerminalResize,
|
||||||
|
stopAllTerminalSessions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user