Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { netcattyBridge } from '../../../infrastructure/services/netcattyBridge';
|
|
|
|
export interface ZmodemTransferEvent {
|
|
type: 'detect' | 'progress' | 'complete' | 'error';
|
|
sessionId: string;
|
|
transferType?: 'upload' | 'download';
|
|
filename?: string;
|
|
transferred?: number;
|
|
total?: number;
|
|
fileIndex?: number;
|
|
fileCount?: number;
|
|
finalizing?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export interface ZmodemTransferState {
|
|
active: boolean;
|
|
transferType: 'upload' | 'download' | null;
|
|
filename: string | null;
|
|
transferred: number;
|
|
total: number;
|
|
fileIndex: number;
|
|
fileCount: number;
|
|
finalizing: boolean;
|
|
completed: boolean;
|
|
startedAt: number | null;
|
|
updatedAt: number | null;
|
|
bytesPerSecond: number | null;
|
|
error: string | null;
|
|
}
|
|
|
|
const initialState: ZmodemTransferState = {
|
|
active: false,
|
|
transferType: null,
|
|
filename: null,
|
|
transferred: 0,
|
|
total: 0,
|
|
fileIndex: 0,
|
|
fileCount: 0,
|
|
finalizing: false,
|
|
completed: false,
|
|
startedAt: null,
|
|
updatedAt: null,
|
|
bytesPerSecond: null,
|
|
error: null,
|
|
};
|
|
|
|
export function reduceZmodemTransferState(
|
|
prev: ZmodemTransferState,
|
|
event: ZmodemTransferEvent,
|
|
now: number = Date.now(),
|
|
): ZmodemTransferState {
|
|
switch (event.type) {
|
|
case 'detect':
|
|
return {
|
|
...initialState,
|
|
active: true,
|
|
transferType: event.transferType ?? null,
|
|
startedAt: now,
|
|
updatedAt: now,
|
|
};
|
|
case 'progress': {
|
|
const transferred = event.transferred ?? prev.transferred;
|
|
const fileChanged = (
|
|
prev.filename !== null
|
|
&& (
|
|
(typeof event.fileIndex === 'number' && event.fileIndex !== prev.fileIndex)
|
|
|| (typeof event.filename === 'string' && event.filename !== prev.filename)
|
|
)
|
|
);
|
|
const previousUpdatedAt = fileChanged ? now : (prev.updatedAt ?? now);
|
|
const elapsedSeconds = Math.max((now - previousUpdatedAt) / 1000, 0);
|
|
const deltaBytes = Math.max(transferred - prev.transferred, 0);
|
|
const bytesPerSecond = elapsedSeconds > 0 && deltaBytes > 0
|
|
? deltaBytes / elapsedSeconds
|
|
: fileChanged
|
|
? null
|
|
: prev.bytesPerSecond;
|
|
|
|
return {
|
|
...prev,
|
|
active: true,
|
|
transferType: event.transferType ?? prev.transferType,
|
|
filename: event.filename ?? prev.filename,
|
|
transferred,
|
|
total: event.total ?? prev.total,
|
|
fileIndex: event.fileIndex ?? prev.fileIndex,
|
|
fileCount: event.fileCount ?? prev.fileCount,
|
|
finalizing: !!event.finalizing,
|
|
completed: false,
|
|
startedAt: prev.startedAt ?? now,
|
|
updatedAt: now,
|
|
bytesPerSecond,
|
|
error: null,
|
|
};
|
|
}
|
|
case 'complete':
|
|
return {
|
|
...prev,
|
|
active: false,
|
|
finalizing: false,
|
|
completed: true,
|
|
updatedAt: now,
|
|
};
|
|
case 'error':
|
|
return {
|
|
...prev,
|
|
active: false,
|
|
finalizing: false,
|
|
completed: false,
|
|
updatedAt: now,
|
|
error: event.error ?? 'Unknown error',
|
|
};
|
|
}
|
|
}
|
|
|
|
export function useZmodemTransfer(sessionId: string | null) {
|
|
const [state, setState] = useState<ZmodemTransferState>(initialState);
|
|
const [overwriteRequest, setOverwriteRequest] = useState<{ requestId: string; filename: string } | null>(null);
|
|
const disposeRef = useRef<(() => void) | null>(null);
|
|
|
|
const disposeExitRef = useRef<(() => void) | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!sessionId) return;
|
|
|
|
const bridge = netcattyBridge.get();
|
|
if (!bridge?.onZmodemEvent) return;
|
|
|
|
disposeRef.current = bridge.onZmodemEvent(sessionId, (event) => {
|
|
setState((prev) => reduceZmodemTransferState(prev, event));
|
|
});
|
|
|
|
const disposeOverwrite = bridge.onZmodemOverwriteRequest?.(sessionId, (payload) => {
|
|
setOverwriteRequest({ requestId: payload.requestId, filename: payload.filename });
|
|
});
|
|
|
|
// If the session exits mid-transfer (disconnect, shell exit, etc.),
|
|
// reset state so the progress indicator doesn't stay stuck.
|
|
disposeExitRef.current = bridge.onSessionExit(sessionId, () => {
|
|
setState(initialState);
|
|
});
|
|
|
|
return () => {
|
|
disposeRef.current?.();
|
|
disposeRef.current = null;
|
|
disposeOverwrite?.();
|
|
disposeExitRef.current?.();
|
|
disposeExitRef.current = null;
|
|
setState(initialState);
|
|
setOverwriteRequest(null);
|
|
};
|
|
}, [sessionId]);
|
|
|
|
const cancel = useCallback(() => {
|
|
if (!sessionId) return;
|
|
const bridge = netcattyBridge.get();
|
|
bridge?.cancelZmodem?.(sessionId);
|
|
}, [sessionId]);
|
|
|
|
const respondOverwrite = useCallback((action: "overwrite" | "skip" | "cancel", applyToRest: boolean) => {
|
|
setOverwriteRequest((req) => {
|
|
if (req) netcattyBridge.get()?.respondZmodemOverwrite?.({ requestId: req.requestId, action, applyToRest });
|
|
return null;
|
|
});
|
|
}, []);
|
|
|
|
return { ...state, cancel, overwriteRequest, respondOverwrite };
|
|
}
|