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:
2026-02-14 11:20:55 -06:00
parent 3c401c4c2b
commit 8daf07819b
3 changed files with 166 additions and 7 deletions

View File

@@ -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;