feat: Add realtime git status updates via WebSocket
- Add file watcher on .git directory in terminal server - Broadcast git-change events to connected clients - Frontend auto-refreshes when changes detected - Visual indicator shows realtime connection status
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useGitApi } from '@/composables/git'
|
||||
import { DiffViewer, FileTree, CommitList, BranchSelector, ProjectTree, FileViewer } from '@/components/git'
|
||||
import { endpoints } from '@/config/endpoints'
|
||||
|
||||
type TabName = 'status' | 'history' | 'compare' | 'files'
|
||||
|
||||
@@ -42,6 +43,63 @@ const compareHead = ref('')
|
||||
// Files tab state
|
||||
const selectedFilePath = ref<string | null>(null)
|
||||
|
||||
// Realtime WebSocket connection
|
||||
let gitSocket: WebSocket | null = null
|
||||
const isRealtime = ref(false)
|
||||
|
||||
function connectGitWatcher() {
|
||||
if (gitSocket?.readyState === WebSocket.OPEN) return
|
||||
|
||||
gitSocket = new WebSocket(endpoints.terminal)
|
||||
|
||||
gitSocket.onopen = () => {
|
||||
isRealtime.value = true
|
||||
console.log('[Git] Realtime connected')
|
||||
}
|
||||
|
||||
gitSocket.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg.type === 'git-change') {
|
||||
console.log('[Git] Change detected, refreshing...')
|
||||
refreshCurrentTab()
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON messages
|
||||
}
|
||||
}
|
||||
|
||||
gitSocket.onclose = () => {
|
||||
isRealtime.value = false
|
||||
console.log('[Git] Realtime disconnected, reconnecting...')
|
||||
setTimeout(connectGitWatcher, 2000)
|
||||
}
|
||||
|
||||
gitSocket.onerror = () => {
|
||||
isRealtime.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectGitWatcher() {
|
||||
if (gitSocket) {
|
||||
gitSocket.close()
|
||||
gitSocket = null
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCurrentTab() {
|
||||
if (activeTab.value === 'status') {
|
||||
await fetchStatus()
|
||||
if (selectedFile.value) {
|
||||
await fetchDiff({ file: selectedFile.value, staged: selectedStaged.value })
|
||||
}
|
||||
} else if (activeTab.value === 'history') {
|
||||
await fetchLog(30)
|
||||
} else if (activeTab.value === 'files') {
|
||||
await fetchFileTree()
|
||||
}
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
@@ -51,6 +109,12 @@ onMounted(async () => {
|
||||
])
|
||||
// Load initial diff
|
||||
await fetchDiff()
|
||||
// Connect realtime watcher
|
||||
connectGitWatcher()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectGitWatcher()
|
||||
})
|
||||
|
||||
// Refresh when tab changes
|
||||
@@ -143,11 +207,16 @@ const totalChanges = computed(() => {
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>Git</h2>
|
||||
<button class="refresh-btn" @click="refresh" :disabled="loading" title="Refresh">
|
||||
<svg :class="{ spinning: loading }" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<span :class="['realtime-indicator', { connected: isRealtime }]" :title="isRealtime ? 'Realtime: connected' : 'Realtime: disconnected'">
|
||||
<svg width="8" height="8" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="currentColor"/></svg>
|
||||
</span>
|
||||
<button class="refresh-btn" @click="refresh" :disabled="loading" title="Refresh">
|
||||
<svg :class="{ spinning: loading }" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="branch-info">
|
||||
@@ -502,6 +571,29 @@ const totalChanges = computed(() => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.realtime-indicator {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.realtime-indicator.connected {
|
||||
color: #22c55e;
|
||||
opacity: 1;
|
||||
animation: pulse-glow 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { filter: drop-shadow(0 0 2px currentColor); }
|
||||
50% { filter: drop-shadow(0 0 6px currentColor); }
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
Reference in New Issue
Block a user