Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 144 additions & 10 deletions src/renderer/src/components/sidepanel/ChatSidePanel.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
<template>
<div
class="chat-side-panel-shell relative h-full min-h-0 shrink-0 overflow-hidden"
:class="{ 'chat-side-panel-shell--resizing': isResizing }"
:style="{ width: `${layoutWidth}px` }"
data-testid="chat-side-panel-shell"
class="chat-side-panel-shell h-full min-h-0 overflow-hidden"
:class="[
isWorkspaceFullscreenActive ? 'absolute inset-0 z-30 w-full' : 'relative shrink-0',
{ 'chat-side-panel-shell--resizing': isResizing }
]"
:style="shellStyle"
:data-workspace-fullscreen="String(isWorkspaceFullscreenActive)"
>
<aside
v-if="props.sessionId"
class="chat-side-panel-surface absolute inset-y-0 right-0 flex h-full min-h-0 w-full flex-col border-l bg-background shadow-lg transition-[transform,opacity,box-shadow] duration-[var(--dc-motion-default)] ease-[var(--dc-ease-out-express)]"
:class="
class="chat-side-panel-surface absolute inset-y-0 flex h-full min-h-0 w-full origin-right flex-col bg-background"
:class="[
isWorkspaceFullscreenActive ? 'inset-x-0 border shadow-xl' : 'right-0 border-l shadow-lg',
panelVisible
? 'translate-x-0 opacity-100'
: 'pointer-events-none translate-x-3 opacity-0 shadow-none'
"
: 'pointer-events-none translate-x-3 opacity-0 shadow-none',
{
'chat-side-panel-surface--fullscreen-enter': fullscreenMotionState === 'expanding',
'chat-side-panel-surface--fullscreen-exit': fullscreenMotionState === 'collapsing'
}
]"
>
<button
v-if="panelVisible"
v-if="panelVisible && !isWorkspaceFullscreenActive"
data-testid="chat-side-panel-resize-handle"
class="absolute inset-y-0 left-0 w-1 -translate-x-1/2 cursor-col-resize"
type="button"
@mousedown="startResize"
Expand All @@ -23,7 +34,7 @@
<div class="flex h-11 items-center justify-between border-b px-3">
<div class="flex items-center gap-1 rounded-lg bg-muted p-0.5">
<button
class="rounded-md px-2.5 py-1 text-xs transition-colors duration-[var(--dc-motion-fast)] ease-[var(--dc-ease-out-soft)]"
class="rounded-md px-2.5 py-1 text-xs transition-colors duration-200 ease-out"
:class="
sidepanelStore.activeTab === 'workspace'
? 'bg-background text-foreground shadow-sm'
Expand All @@ -35,7 +46,7 @@
{{ t('chat.workspace.title') }}
</button>
<button
class="rounded-md px-2.5 py-1 text-xs transition-colors duration-[var(--dc-motion-fast)] ease-[var(--dc-ease-out-soft)]"
class="rounded-md px-2.5 py-1 text-xs transition-colors duration-200 ease-out"
:class="
sidepanelStore.activeTab === 'browser'
? 'bg-background text-foreground shadow-sm'
Expand All @@ -57,6 +68,8 @@
v-if="sidepanelStore.activeTab === 'workspace'"
:session-id="props.sessionId"
:workspace-path="props.workspacePath"
:is-fullscreen="isWorkspaceFullscreenActive"
@toggle-fullscreen="toggleWorkspaceFullscreen"
/>
<BrowserPanel v-else :session-id="props.sessionId" />
</aside>
Expand All @@ -82,17 +95,31 @@ const { t } = useI18n()
const sidepanelStore = useSidepanelStore()
const browserClient = createBrowserClient()
const PANEL_MOTION_MS = 220
const FULLSCREEN_MOTION_MS = 180
let stopBrowserOpenRequestedListener: (() => void) | null = null
let resizeCleanup: (() => void) | null = null
let pendingResizeWidth: number | null = null
let resizeFrame: number | null = null
let panelMotionTimer: number | null = null
let panelMotionFrame: number | null = null
let fullscreenMotionTimer: number | null = null

const shouldShow = computed(() => sidepanelStore.open && Boolean(props.sessionId))
const layoutWidth = ref(shouldShow.value ? sidepanelStore.width : 0)
const panelVisible = ref(shouldShow.value)
const isResizing = ref(false)
const isWorkspaceFullscreen = ref(false)
const fullscreenMotionState = ref<'expanding' | 'collapsing' | null>(null)

const isWorkspaceFullscreenActive = computed(() => {
return isWorkspaceFullscreen.value && shouldShow.value && sidepanelStore.activeTab === 'workspace'
})

const shellStyle = computed(() => {
return {
width: isWorkspaceFullscreenActive.value ? '100%' : `${layoutWidth.value}px`
}
})

const handleBrowserOpenRequested = (payload: {
sessionId: string
Expand All @@ -119,6 +146,15 @@ const clearPanelMotionHandles = () => {
}
}

const clearFullscreenMotionHandle = () => {
if (fullscreenMotionTimer !== null) {
window.clearTimeout(fullscreenMotionTimer)
fullscreenMotionTimer = null
}

fullscreenMotionState.value = null
}

const applyPendingResize = () => {
resizeFrame = null
if (pendingResizeWidth === null) {
Expand All @@ -144,8 +180,32 @@ const stopResizeTracking = () => {
}
}

const resetWorkspaceFullscreen = () => {
isWorkspaceFullscreen.value = false
clearFullscreenMotionHandle()
}

const toggleWorkspaceFullscreen = () => {
if (!shouldShow.value || sidepanelStore.activeTab !== 'workspace') {
return
}

clearFullscreenMotionHandle()
fullscreenMotionState.value = isWorkspaceFullscreen.value ? 'collapsing' : 'expanding'
fullscreenMotionTimer = window.setTimeout(() => {
fullscreenMotionTimer = null
fullscreenMotionState.value = null
}, FULLSCREEN_MOTION_MS)
isWorkspaceFullscreen.value = !isWorkspaceFullscreen.value
}

const startResize = (event: MouseEvent) => {
event.preventDefault()

if (isWorkspaceFullscreenActive.value) {
return
}

stopResizeTracking()
isResizing.value = true

Expand Down Expand Up @@ -178,6 +238,10 @@ watch(shouldShow, (visible) => {
clearPanelMotionHandles()
stopResizeTracking()

if (!visible) {
resetWorkspaceFullscreen()
}

if (visible) {
layoutWidth.value = sidepanelStore.width
panelMotionFrame = window.requestAnimationFrame(() => {
Expand All @@ -196,6 +260,24 @@ watch(shouldShow, (visible) => {
}, PANEL_MOTION_MS)
})

watch(
() => sidepanelStore.activeTab,
(activeTab) => {
if (activeTab !== 'workspace') {
resetWorkspaceFullscreen()
}
}
)

watch(
() => props.sessionId,
(sessionId, previousSessionId) => {
if (!sessionId || sessionId !== previousSessionId) {
resetWorkspaceFullscreen()
}
}
)

watch(
() => sidepanelStore.width,
(width) => {
Expand All @@ -213,6 +295,7 @@ onMounted(() => {

onBeforeUnmount(() => {
clearPanelMotionHandles()
clearFullscreenMotionHandle()
stopResizeTracking()
stopBrowserOpenRequestedListener?.()
stopBrowserOpenRequestedListener = null
Expand All @@ -222,21 +305,72 @@ onBeforeUnmount(() => {
<style scoped>
.chat-side-panel-shell {
contain: layout style paint;
transition-duration: var(--dc-motion-default);
transition-property: width;
transition-timing-function: var(--dc-ease-out-express);
}

.chat-side-panel-surface {
backface-visibility: hidden;
transform: translateZ(0);
transition-duration: var(--dc-motion-default);
transition-property: transform, opacity, box-shadow, border-radius;
transition-timing-function: var(--dc-ease-out-express);
will-change: transform, opacity;
}

.chat-side-panel-surface--fullscreen-enter {
animation: workspace-panel-fullscreen-enter 180ms var(--dc-ease-out-express);
}

.chat-side-panel-surface--fullscreen-exit {
animation: workspace-panel-fullscreen-exit 180ms var(--dc-ease-out-express);
}

.chat-side-panel-shell--resizing .chat-side-panel-surface {
transition: none;
}

.chat-side-panel-shell--resizing {
transition: none;
}

@keyframes workspace-panel-fullscreen-enter {
from {
opacity: 0.94;
transform: translateZ(0) scale(0.985);
}

to {
opacity: 1;
transform: translateZ(0) scale(1);
}
}

@keyframes workspace-panel-fullscreen-exit {
from {
opacity: 0.96;
transform: translateZ(0) scale(1.01);
}

to {
opacity: 1;
transform: translateZ(0) scale(1);
}
}

@media (prefers-reduced-motion: reduce) {
.chat-side-panel-shell {
transition: none;
}

.chat-side-panel-surface {
transition: none;
}

.chat-side-panel-surface--fullscreen-enter,
.chat-side-panel-surface--fullscreen-exit {
animation: none;
}
}
</style>
4 changes: 4 additions & 0 deletions src/renderer/src/components/sidepanel/WorkspacePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
:git-diff="selectedGitDiff"
:loading-file-preview="loadingFilePreview"
:loading-git-diff="loadingGitDiff"
:is-fullscreen="props.isFullscreen"
@toggle-fullscreen="emit('toggle-fullscreen')"
/>
</div>
</template>
Expand All @@ -161,10 +163,12 @@ import type { WorkspaceGitFileChange } from '@shared/presenter'
const props = defineProps<{
sessionId: string
workspacePath: string | null
isFullscreen?: boolean
}>()

const emit = defineEmits<{
'update:workspacePath': [path: string | null]
'toggle-fullscreen': []
}>()

type ArtifactItem = WorkspaceArtifactContext & {
Expand Down
29 changes: 27 additions & 2 deletions src/renderer/src/components/sidepanel/WorkspaceViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@
</button>
</div>

<Button
variant="ghost"
size="icon"
class="h-7 w-7"
data-testid="workspace-viewer-fullscreen-toggle"
:title="fullscreenToggleLabel"
:aria-label="fullscreenToggleLabel"
@click="emit('toggle-fullscreen')"
>
<Icon
:icon="props.isFullscreen ? 'lucide:minimize-2' : 'lucide:maximize-2'"
class="h-4 w-4"
/>
</Button>

<Button
v-if="openFilePath"
variant="outline"
Expand Down Expand Up @@ -76,15 +91,15 @@
>
{{ t('chat.workspace.git.staged') }}
</h4>
<pre class="whitespace-pre-wrap break-words">{{ props.gitDiff.staged }}</pre>
<pre class="whitespace-pre-wrap wrap-break-word">{{ props.gitDiff.staged }}</pre>
</section>
<section v-if="props.gitDiff.unstaged">
<h4
class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
>
{{ t('chat.workspace.git.unstaged') }}
</h4>
<pre class="whitespace-pre-wrap break-words">{{ props.gitDiff.unstaged }}</pre>
<pre class="whitespace-pre-wrap wrap-break-word">{{ props.gitDiff.unstaged }}</pre>
</section>
<div
v-if="!props.gitDiff.staged && !props.gitDiff.unstaged"
Expand Down Expand Up @@ -131,6 +146,7 @@

<script setup lang="ts">
import { computed } from 'vue'
import { Icon } from '@iconify/vue'
import { useI18n } from 'vue-i18n'
import { Button } from '@shadcn/components/ui/button'
import { createWorkspaceClient } from '@api/WorkspaceClient'
Expand All @@ -149,6 +165,11 @@ const props = defineProps<{
gitDiff: WorkspaceGitDiff | null
loadingFilePreview: boolean
loadingGitDiff: boolean
isFullscreen?: boolean
}>()

const emit = defineEmits<{
'toggle-fullscreen': []
}>()

const { t } = useI18n()
Expand Down Expand Up @@ -251,6 +272,10 @@ const emptyMessage = computed(() => {
return t('chat.workspace.title')
})

const fullscreenToggleLabel = computed(() => {
return props.isFullscreen ? t('common.restore') : t('common.maximize')
})

const handleOpenFile = async () => {
if (!openFilePath.value) {
return
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/views/ChatTabView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="flex h-full min-h-0 w-full flex-row overflow-hidden">
<div class="relative flex h-full min-h-0 w-full flex-row overflow-hidden">
<div
class="relative flex h-full min-h-0 min-w-0 w-0 flex-1 transition-[width] duration-200 ease-out"
>
Expand Down
Loading