Skip to content

Commit bc8978d

Browse files
Add JSON fallback for worktrees endpoint when SSE fails
SSE doesn't work through Cloudflare tunnels (502 Bad Gateway). Added: - X-Accel-Buffering header to disable proxy buffering - Immediate ping comment to establish SSE connection faster - /api/worktrees/json endpoint as fallback - Frontend auto-fallback from SSE to JSON on connection error
1 parent 68cb1ff commit bc8978d

7 files changed

Lines changed: 144 additions & 10 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"name": "vibora",
99
"source": "./plugins/vibora",
1010
"description": "Task orchestration for Claude Code",
11-
"version": "3.2.3"
11+
"version": "3.2.4"
1212
}
1313
]
1414
}

desktop/neutralino.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
33
"applicationId": "io.vibora.desktop",
4-
"version": "3.2.3",
4+
"version": "3.2.4",
55
"defaultMode": "window",
66
"port": 0,
77
"documentRoot": "/resources/",
@@ -26,7 +26,7 @@
2626
],
2727
"globalVariables": {
2828
"APP_NAME": "Vibora",
29-
"APP_VERSION": "3.2.3"
29+
"APP_VERSION": "3.2.4"
3030
},
3131
"modes": {
3232
"window": {

frontend/hooks/use-worktrees.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ interface UseWorktreesReturn {
1515
refetch: () => void
1616
}
1717

18+
interface WorktreesJsonResponse {
19+
worktrees: (WorktreeBasic & Partial<WorktreeDetails>)[]
20+
summary: WorktreesSummary
21+
}
22+
1823
export function useWorktrees(): UseWorktreesReturn {
1924
const [worktreesMap, setWorktreesMap] = useState<Map<string, Worktree>>(new Map())
2025
const [summary, setSummary] = useState<WorktreesSummary | null>(null)
@@ -23,8 +28,47 @@ export function useWorktrees(): UseWorktreesReturn {
2328
const [error, setError] = useState<Error | null>(null)
2429
const eventSourceRef = useRef<EventSource | null>(null)
2530
const pendingDetailsRef = useRef<number>(0)
31+
const useJsonFallbackRef = useRef(false)
32+
33+
// JSON fallback for environments where SSE doesn't work (e.g., Cloudflare tunnels)
34+
const fetchJson = useCallback(async () => {
35+
setIsLoading(true)
36+
setError(null)
37+
try {
38+
const response = await fetch(`${API_BASE}/api/worktrees/json`)
39+
if (!response.ok) throw new Error('Failed to fetch worktrees')
40+
const data: WorktreesJsonResponse = await response.json()
41+
42+
setWorktreesMap(
43+
new Map(
44+
data.worktrees.map((w) => [
45+
w.path,
46+
{
47+
...w,
48+
size: w.size || 0,
49+
sizeFormatted: w.sizeFormatted || '0 B',
50+
branch: w.branch || 'unknown',
51+
},
52+
])
53+
)
54+
)
55+
setSummary(data.summary)
56+
setIsLoading(false)
57+
setIsLoadingDetails(false)
58+
} catch (err) {
59+
setError(err instanceof Error ? err : new Error('Failed to load worktrees'))
60+
setIsLoading(false)
61+
setIsLoadingDetails(false)
62+
}
63+
}, [])
2664

2765
const connect = useCallback(() => {
66+
// If we've determined SSE doesn't work, use JSON fallback
67+
if (useJsonFallbackRef.current) {
68+
fetchJson()
69+
return
70+
}
71+
2872
// Close existing connection
2973
eventSourceRef.current?.close()
3074

@@ -98,12 +142,13 @@ export function useWorktrees(): UseWorktreesReturn {
98142
})
99143

100144
eventSource.onerror = () => {
101-
setError(new Error('Connection lost'))
102-
setIsLoading(false)
103-
setIsLoadingDetails(false)
104145
eventSource.close()
146+
// SSE failed, try JSON fallback
147+
log.viewer.info('SSE connection failed, trying JSON fallback')
148+
useJsonFallbackRef.current = true
149+
fetchJson()
105150
}
106-
}, [])
151+
}, [fetchJson])
107152

108153
useEffect(() => {
109154
connect()

frontend/routes/terminals/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ const TerminalsView = observer(function TerminalsView() {
9595
newTerminalIds,
9696
pendingTabCreation,
9797
lastCreatedTabId,
98-
clearLastCreatedTabId,
9998
} = useTerminalStore()
10099

101100
// State for tab edit dialog

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vibora",
33
"private": true,
4-
"version": "3.2.3",
4+
"version": "3.2.4",
55
"description": "The Vibe Engineer's Cockpit",
66
"license": "PolyForm-Shield-1.0.0",
77
"type": "module",

plugins/vibora/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vibora",
33
"description": "Vibora task orchestration for Claude Code",
4-
"version": "3.2.3",
4+
"version": "3.2.4",
55
"author": {
66
"name": "Vibora"
77
},

server/routes/worktrees.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ const app = new Hono()
9898

9999
// GET /api/worktrees - Stream worktrees via SSE for progressive loading
100100
app.get('/', (c) => {
101+
// Disable proxy buffering for SSE (required for Cloudflare tunnels)
102+
c.header('X-Accel-Buffering', 'no')
103+
101104
return streamSSE(c, async (stream) => {
105+
// Send immediate comment to establish connection (helps with proxies/tunnels)
106+
await stream.write(': ping\n\n')
107+
102108
const worktreeBasePath = getWorktreeBasePath()
103109

104110
// Handle missing directory
@@ -223,6 +229,90 @@ app.get('/', (c) => {
223229
})
224230
})
225231

232+
// GET /api/worktrees/json - JSON fallback for environments where SSE doesn't work (e.g., Cloudflare tunnels)
233+
app.get('/json', async (c) => {
234+
const worktreeBasePath = getWorktreeBasePath()
235+
236+
// Handle missing directory
237+
if (!fs.existsSync(worktreeBasePath)) {
238+
return c.json({
239+
worktrees: [],
240+
summary: {
241+
total: 0,
242+
orphaned: 0,
243+
totalSize: 0,
244+
totalSizeFormatted: '0 B',
245+
},
246+
})
247+
}
248+
249+
// Get all tasks to build a map of worktreePath -> task
250+
const allTasks = db.select().from(tasks).all()
251+
const worktreeToTask = new Map<string, (typeof allTasks)[0]>()
252+
for (const task of allTasks) {
253+
if (task.worktreePath) {
254+
worktreeToTask.set(task.worktreePath, task)
255+
}
256+
}
257+
258+
// Read all directories in worktreeBasePath
259+
const entries = fs.readdirSync(worktreeBasePath, { withFileTypes: true })
260+
const worktrees: (WorktreeBasic & Partial<WorktreeDetails>)[] = []
261+
262+
for (const entry of entries) {
263+
if (!entry.isDirectory()) continue
264+
265+
const fullPath = path.join(worktreeBasePath, entry.name)
266+
267+
// Check if it's a git worktree (has .git file or directory)
268+
const gitPath = path.join(fullPath, '.git')
269+
if (!fs.existsSync(gitPath)) continue
270+
271+
const stats = fs.statSync(fullPath)
272+
const linkedTask = worktreeToTask.get(fullPath)
273+
274+
// Get size and branch in parallel
275+
const [size, branch] = await Promise.all([
276+
getDirectorySizeAsync(fullPath),
277+
getGitBranchAsync(fullPath),
278+
])
279+
280+
worktrees.push({
281+
path: fullPath,
282+
name: entry.name,
283+
lastModified: stats.mtime.toISOString(),
284+
isOrphaned: !linkedTask,
285+
taskId: linkedTask?.id,
286+
taskTitle: linkedTask?.title,
287+
taskStatus: linkedTask?.status,
288+
repoPath: linkedTask?.repoPath,
289+
size,
290+
sizeFormatted: formatBytes(size),
291+
branch,
292+
})
293+
}
294+
295+
// Sort: orphaned first, then by last modified (newest first)
296+
worktrees.sort((a, b) => {
297+
if (a.isOrphaned !== b.isOrphaned) {
298+
return a.isOrphaned ? -1 : 1
299+
}
300+
return new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
301+
})
302+
303+
const totalSize = worktrees.reduce((sum, w) => sum + (w.size || 0), 0)
304+
305+
return c.json({
306+
worktrees,
307+
summary: {
308+
total: worktrees.length,
309+
orphaned: worktrees.filter((w) => w.isOrphaned).length,
310+
totalSize,
311+
totalSizeFormatted: formatBytes(totalSize),
312+
},
313+
})
314+
})
315+
226316
// DELETE /api/worktrees - Delete a worktree (optionally delete linked task)
227317
app.delete('/', async (c) => {
228318
try {

0 commit comments

Comments
 (0)