mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Integrated terminal (#1295)
* Add terminal tab * Add basic terminal * Style terminal * Add keybinding * Auto scroll: * Mock for jest test
This commit is contained in:
@@ -1,34 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
|
<div class="flex flex-col h-full">
|
||||||
<TabList pt:tabList="border-none">
|
<Tabs v-model:value="bottomPanelStore.activeBottomPanelTabId">
|
||||||
<div class="w-full flex justify-between">
|
<TabList pt:tabList="border-none">
|
||||||
<div class="tabs-container">
|
<div class="w-full flex justify-between">
|
||||||
<Tab
|
<div class="tabs-container">
|
||||||
v-for="tab in bottomPanelStore.bottomPanelTabs"
|
<Tab
|
||||||
:key="tab.id"
|
v-for="tab in bottomPanelStore.bottomPanelTabs"
|
||||||
:value="tab.id"
|
:key="tab.id"
|
||||||
class="p-3 border-none"
|
:value="tab.id"
|
||||||
>
|
class="p-3 border-none"
|
||||||
<span class="font-bold">
|
>
|
||||||
{{ tab.title.toUpperCase() }}
|
<span class="font-bold">
|
||||||
</span>
|
{{ tab.title.toUpperCase() }}
|
||||||
</Tab>
|
</span>
|
||||||
|
</Tab>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
class="justify-self-end"
|
||||||
|
icon="pi pi-times"
|
||||||
|
severity="secondary"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="bottomPanelStore.bottomPanelVisible = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
</TabList>
|
||||||
class="justify-self-end"
|
</Tabs>
|
||||||
icon="pi pi-times"
|
<!-- h-0 to force the div to flex-grow -->
|
||||||
severity="secondary"
|
<div class="flex-grow h-0">
|
||||||
size="small"
|
<ExtensionSlot
|
||||||
text
|
v-if="
|
||||||
@click="bottomPanelStore.bottomPanelVisible = false"
|
bottomPanelStore.bottomPanelVisible &&
|
||||||
/>
|
bottomPanelStore.activeBottomPanelTab
|
||||||
</div>
|
"
|
||||||
</TabList>
|
:extension="bottomPanelStore.activeBottomPanelTab"
|
||||||
</Tabs>
|
/>
|
||||||
<ExtensionSlot
|
</div>
|
||||||
v-if="bottomPanelStore.activeBottomPanelTab"
|
</div>
|
||||||
:extension="bottomPanelStore.activeBottomPanelTab"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
61
src/components/bottomPanel/tabs/IntegratedTerminal.vue
Normal file
61
src/components/bottomPanel/tabs/IntegratedTerminal.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-terminal rounded-none h-full w-full">
|
||||||
|
<ScrollPanel class="h-full w-full" ref="scrollPanelRef">
|
||||||
|
<pre class="px-4 whitespace-pre-wrap">{{ log }}</pre>
|
||||||
|
</ScrollPanel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ScrollPanel from 'primevue/scrollpanel'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const log = ref<string>('')
|
||||||
|
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
|
||||||
|
/**
|
||||||
|
* Whether the user has scrolled to the bottom of the terminal.
|
||||||
|
* This is used to prevent the terminal from scrolling to the bottom
|
||||||
|
* when new logs are fetched.
|
||||||
|
*/
|
||||||
|
const scrolledToBottom = ref(false)
|
||||||
|
|
||||||
|
let intervalId: number = 0
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const element = scrollPanelRef.value?.$el
|
||||||
|
const scrollContainer = element?.querySelector('.p-scrollpanel-content')
|
||||||
|
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.addEventListener('scroll', () => {
|
||||||
|
scrolledToBottom.value =
|
||||||
|
scrollContainer.scrollTop + scrollContainer.clientHeight ===
|
||||||
|
scrollContainer.scrollHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.scrollTop = scrollContainer.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(log, () => {
|
||||||
|
if (scrolledToBottom.value) {
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
log.value = await api.getLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchLogs()
|
||||||
|
scrollToBottom()
|
||||||
|
intervalId = window.setInterval(fetchLogs, 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.clearInterval(intervalId)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
14
src/hooks/bottomPanelTabs/integratedTerminalTab.ts
Normal file
14
src/hooks/bottomPanelTabs/integratedTerminalTab.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import IntegratedTerminal from '@/components/bottomPanel/tabs/IntegratedTerminal.vue'
|
||||||
|
import { BottomPanelExtension } from '@/types/extensionTypes'
|
||||||
|
|
||||||
|
export const useIntegratedTerminalTab = (): BottomPanelExtension => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
return {
|
||||||
|
id: 'integrated-terminal',
|
||||||
|
title: t('terminal'),
|
||||||
|
component: markRaw(IntegratedTerminal),
|
||||||
|
type: 'vue'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { createI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
en: {
|
en: {
|
||||||
|
terminal: 'Terminal',
|
||||||
videoFailedToLoad: 'Video failed to load',
|
videoFailedToLoad: 'Video failed to load',
|
||||||
extensionName: 'Extension Name',
|
extensionName: 'Extension Name',
|
||||||
reloadToApplyChanges: 'Reload to apply changes',
|
reloadToApplyChanges: 'Reload to apply changes',
|
||||||
@@ -113,6 +114,7 @@ const messages = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
terminal: '终端',
|
||||||
videoFailedToLoad: '视频加载失败',
|
videoFailedToLoad: '视频加载失败',
|
||||||
extensionName: '扩展名称',
|
extensionName: '扩展名称',
|
||||||
reloadToApplyChanges: '重新加载以应用更改',
|
reloadToApplyChanges: '重新加载以应用更改',
|
||||||
@@ -223,6 +225,7 @@ const messages = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
|
terminal: 'Терминал',
|
||||||
videoFailedToLoad: 'Видео не удалось загрузить',
|
videoFailedToLoad: 'Видео не удалось загрузить',
|
||||||
extensionName: 'Название расширения',
|
extensionName: 'Название расширения',
|
||||||
reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения',
|
reloadToApplyChanges: 'Перезагрузите, чтобы применить изменения',
|
||||||
|
|||||||
@@ -160,5 +160,12 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
|||||||
},
|
},
|
||||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
||||||
targetSelector: '#graph-canvas'
|
targetSelector: '#graph-canvas'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
combo: {
|
||||||
|
key: '`',
|
||||||
|
ctrl: true
|
||||||
|
},
|
||||||
|
commandId: 'Workspace.ToggleBottomPanelTab.integrated-terminal'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { BottomPanelExtension } from '@/types/extensionTypes'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useIntegratedTerminalTab } from '@/hooks/bottomPanelTabs/integratedTerminalTab'
|
||||||
|
|
||||||
export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
||||||
const bottomPanelVisible = ref(false)
|
const bottomPanelVisible = ref(false)
|
||||||
@@ -26,7 +27,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
|||||||
activeBottomPanelTabId.value = tabId
|
activeBottomPanelTabId.value = tabId
|
||||||
}
|
}
|
||||||
const toggleBottomPanelTab = (tabId: string) => {
|
const toggleBottomPanelTab = (tabId: string) => {
|
||||||
if (activeBottomPanelTabId.value === tabId) {
|
if (activeBottomPanelTabId.value === tabId && bottomPanelVisible.value) {
|
||||||
bottomPanelVisible.value = false
|
bottomPanelVisible.value = false
|
||||||
} else {
|
} else {
|
||||||
activeBottomPanelTabId.value = tabId
|
activeBottomPanelTabId.value = tabId
|
||||||
@@ -46,6 +47,10 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerCoreBottomPanelTabs = () => {
|
||||||
|
registerBottomPanelTab(useIntegratedTerminalTab())
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bottomPanelVisible,
|
bottomPanelVisible,
|
||||||
toggleBottomPanel,
|
toggleBottomPanel,
|
||||||
@@ -54,6 +59,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
|||||||
activeBottomPanelTabId,
|
activeBottomPanelTabId,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
toggleBottomPanelTab,
|
toggleBottomPanelTab,
|
||||||
registerBottomPanelTab
|
registerBottomPanelTab,
|
||||||
|
registerCoreBottomPanelTabs
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { useKeybindingStore } from '@/stores/keybindingStore'
|
|||||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||||
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||||
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||||
|
|
||||||
setupAutoQueueHandler()
|
setupAutoQueueHandler()
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ const init = () => {
|
|||||||
settingStore.addSettings(app.ui.settings)
|
settingStore.addSettings(app.ui.settings)
|
||||||
useKeybindingStore().loadCoreKeybindings()
|
useKeybindingStore().loadCoreKeybindings()
|
||||||
useSidebarTabStore().registerCoreSidebarTabs()
|
useSidebarTabStore().registerCoreSidebarTabs()
|
||||||
|
useBottomPanelStore().registerCoreBottomPanelTabs()
|
||||||
app.extensionManager = useWorkspaceStore()
|
app.extensionManager = useWorkspaceStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ module.exports = async function () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
jest.mock('@/stores/workspace/bottomPanelStore', () => {
|
||||||
|
return {
|
||||||
|
toggleBottomPanel: jest.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
jest.mock('vue-i18n', () => {
|
jest.mock('vue-i18n', () => {
|
||||||
return {
|
return {
|
||||||
useI18n: jest.fn()
|
useI18n: jest.fn()
|
||||||
|
|||||||
Reference in New Issue
Block a user