Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/client/RepoView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ interface ToastState {

export function RepoView({ repoPath }: { repoPath: string }) {
const [credReq, setCredReq] = React.useState<{ id: string; prompt: string } | null>(null);
useLiveUpdates(repoPath, (e) => setCredReq({ id: e.id, prompt: e.prompt }));
const connection = useLiveUpdates(repoPath, (e) => setCredReq({ id: e.id, prompt: e.prompt }));
const [settings, setSetting] = useSettings();

const repoQ = useRepo(repoPath);
Expand Down Expand Up @@ -682,6 +682,11 @@ export function RepoView({ repoPath }: { repoPath: string }) {
<div style={{ flex: 1 }} />

<div className="v3-actions">
{connection === 'reconnecting' && (
<span className="conn-badge fade-in" title="Live-update connection dropped — retrying…">
<span className="conn-dot" /> Reconnecting…
</span>
)}
<button
className="tb-btn"
disabled={actions.fetchRemote.isPending}
Expand Down
14 changes: 12 additions & 2 deletions src/client/api/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ import React from 'react';
import { useQueryClient } from '@tanstack/react-query';
import type { ChangeEvent, CredentialRequestEvent } from '../../shared/types';

export type ConnectionState = 'connected' | 'reconnecting';

/** Subscribes to the repo's SSE feed: change events invalidate queries,
credential events surface the askpass modal. */
export function useLiveUpdates(path: string, onCredential?: (e: CredentialRequestEvent) => void) {
credential events surface the askpass modal. Returns the live-connection
state so callers can surface a disconnect/reconnecting indicator — the
browser retries a dropped EventSource on its own, so 'reconnecting' just
tracks the gap between an 'error' and the next 'open'. */
export function useLiveUpdates(path: string, onCredential?: (e: CredentialRequestEvent) => void): ConnectionState {
const qc = useQueryClient();
const credRef = React.useRef(onCredential);
credRef.current = onCredential;
const [connection, setConnection] = React.useState<ConnectionState>('connected');
React.useEffect(() => {
setConnection('connected');
const es = new EventSource('/api/events?path=' + encodeURIComponent(path));
es.addEventListener('open', () => setConnection('connected'));
es.addEventListener('error', () => setConnection('reconnecting'));
es.addEventListener('credential', (e) => {
try {
credRef.current?.(JSON.parse((e as MessageEvent).data) as CredentialRequestEvent);
Expand Down Expand Up @@ -39,4 +48,5 @@ export function useLiveUpdates(path: string, onCredential?: (e: CredentialReques
});
return () => es.close();
}, [qc, path]);
return connection;
}
4 changes: 4 additions & 0 deletions src/client/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ body {
.dh-sub .mono { color: var(--tx-lo); }
.peek-tag { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-family: var(--mono); color: var(--accent); border: 1px solid var(--accent-line); border-radius: 4px; padding: 1px 6px; }

.conn-badge { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--yellow); border: 1px solid var(--yellow); border-radius: 6px; padding: 4px 9px; margin-right: 2px; }
.conn-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--yellow); animation: conn-pulse 1.1s ease-in-out infinite; }
@keyframes conn-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

.diff-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-bottom: 1px solid var(--line-soft); flex-shrink: 0; }
.diff-stat { display: inline-flex; align-items: center; gap: 9px; font-family: var(--mono); font-size: 12px; }
.diff-stat .a { color: var(--add); } .diff-stat .d { color: var(--del); }
Expand Down