feat(ui): add desktop git sidebar + terminal dock and improve in-app PR workflow (#362)
* feat: add unified dropdown with services content in header * feat: add right Git sidebar with resizable panel * feat: implement responsive panel auto-toggle and terminal rehydration - Auto-close the right sidebar when width is below a threshold and auto-open it when space permits - Auto-close the bottom terminal when height is below a threshold and auto-open it when enough space - Apply a dedicated rehydrated streaming configuration for terminal sessions to optimize reconnect behavior * feat: enhance PR view with status caching and annotations * feat(ui): enable chat dispatch in PullRequestSection * feat(TerminalView): adjust layout * feat: refine chat input layout and text selection menu * fix(ui): show empty state in GitView when no changes * feat(git): update PR actions styling and create PR button
This commit is contained in:
committed by
GitHub
parent
3f29b2c6a2
commit
5b0a97d170
@@ -64,11 +64,12 @@ interface TerminalViewportProps {
|
||||
fontSize: number;
|
||||
className?: string;
|
||||
enableTouchScroll?: boolean;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportProps>(
|
||||
(
|
||||
{ sessionKey, chunks, onInput, onResize, theme, fontFamily, fontSize, className, enableTouchScroll },
|
||||
{ sessionKey, chunks, onInput, onResize, theme, fontFamily, fontSize, className, enableTouchScroll, autoFocus = true },
|
||||
ref
|
||||
) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -94,6 +95,7 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
const refocusTimeoutRef = React.useRef<number | null>(null);
|
||||
const keydownProbeTimeoutRef = React.useRef<number | null>(null);
|
||||
const lastObservedValueRef = React.useRef('');
|
||||
const cursorBlinkStateRef = React.useRef<boolean | null>(null);
|
||||
const [, forceRender] = React.useReducer((x) => x + 1, 0);
|
||||
const [terminalReadyVersion, bumpTerminalReady] = React.useReducer((x) => x + 1, 0);
|
||||
|
||||
@@ -157,6 +159,36 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
});
|
||||
}, [useHiddenInputOverlay]);
|
||||
|
||||
const setTerminalCursorBlink = React.useCallback((enabled: boolean) => {
|
||||
if (cursorBlinkStateRef.current === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const terminal = terminalRef.current as unknown as {
|
||||
setOption?: (key: string, value: unknown) => void;
|
||||
options?: { cursorBlink?: boolean };
|
||||
} | null;
|
||||
|
||||
if (!terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof terminal.setOption === 'function') {
|
||||
terminal.setOption('cursorBlink', enabled);
|
||||
cursorBlinkStateRef.current = enabled;
|
||||
return;
|
||||
}
|
||||
|
||||
if (terminal.options) {
|
||||
terminal.options.cursorBlink = enabled;
|
||||
cursorBlinkStateRef.current = enabled;
|
||||
}
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
}, []);
|
||||
|
||||
const useTextInput = useHiddenInputOverlay && isAndroid;
|
||||
|
||||
const focusHiddenInput = React.useCallback((clientX?: number, clientY?: number) => {
|
||||
@@ -198,6 +230,16 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
}
|
||||
}, [useTextInput]);
|
||||
|
||||
const focusTerminalInput = React.useCallback(() => {
|
||||
if (useHiddenInputOverlay) {
|
||||
focusHiddenInput();
|
||||
setTerminalCursorBlink(true);
|
||||
return;
|
||||
}
|
||||
terminalRef.current?.focus();
|
||||
setTerminalCursorBlink(true);
|
||||
}, [focusHiddenInput, setTerminalCursorBlink, useHiddenInputOverlay]);
|
||||
|
||||
const readEditableValue = React.useCallback((target: HTMLElement) => {
|
||||
if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) {
|
||||
return target.value;
|
||||
@@ -839,6 +881,29 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
|
||||
container.tabIndex = useHiddenInputOverlay ? -1 : 0;
|
||||
|
||||
const handleTerminalTextareaFocus = () => {
|
||||
setTerminalCursorBlink(true);
|
||||
};
|
||||
|
||||
const handleTerminalTextareaBlur = () => {
|
||||
setTerminalCursorBlink(false);
|
||||
};
|
||||
|
||||
const handleDocumentFocusIn = (event: FocusEvent) => {
|
||||
const target = event.target as Node | null;
|
||||
if (target && container.contains(target)) {
|
||||
setTerminalCursorBlink(true);
|
||||
return;
|
||||
}
|
||||
setTerminalCursorBlink(false);
|
||||
};
|
||||
|
||||
const handleWindowBlur = () => {
|
||||
setTerminalCursorBlink(false);
|
||||
};
|
||||
|
||||
let localTerminalTextarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
const initialize = async () => {
|
||||
try {
|
||||
const ghostty = await getGhostty();
|
||||
@@ -859,6 +924,16 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(container);
|
||||
bumpTerminalReady();
|
||||
cursorBlinkStateRef.current = false;
|
||||
|
||||
localTerminalTextarea =
|
||||
(terminal as unknown as { textarea?: HTMLTextAreaElement | null }).textarea
|
||||
?? container.querySelector('textarea');
|
||||
|
||||
if (localTerminalTextarea) {
|
||||
localTerminalTextarea.addEventListener('focus', handleTerminalTextareaFocus);
|
||||
localTerminalTextarea.addEventListener('blur', handleTerminalTextareaBlur);
|
||||
}
|
||||
|
||||
disableTerminalTextareas();
|
||||
|
||||
@@ -880,10 +955,6 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
|
||||
fitTerminal();
|
||||
setupTouchScroll();
|
||||
if (!useHiddenInputOverlay) {
|
||||
terminal.focus();
|
||||
}
|
||||
|
||||
localDisposables = [
|
||||
terminal.onData((data: string) => {
|
||||
inputHandlerRef.current(data);
|
||||
@@ -907,12 +978,22 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
|
||||
void initialize();
|
||||
|
||||
document.addEventListener('focusin', handleDocumentFocusIn, true);
|
||||
window.addEventListener('blur', handleWindowBlur);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
touchScrollCleanupRef.current?.();
|
||||
touchScrollCleanupRef.current = null;
|
||||
|
||||
document.removeEventListener('focusin', handleDocumentFocusIn, true);
|
||||
window.removeEventListener('blur', handleWindowBlur);
|
||||
|
||||
localDisposables.forEach((disposable) => disposable.dispose());
|
||||
if (localTerminalTextarea) {
|
||||
localTerminalTextarea.removeEventListener('focus', handleTerminalTextareaFocus);
|
||||
localTerminalTextarea.removeEventListener('blur', handleTerminalTextareaBlur);
|
||||
}
|
||||
localResizeObserver?.disconnect();
|
||||
localTextareaObserver?.disconnect();
|
||||
|
||||
@@ -921,9 +1002,10 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
fitAddonRef.current = null;
|
||||
viewportRef.current = null;
|
||||
lastReportedSizeRef.current = null;
|
||||
cursorBlinkStateRef.current = null;
|
||||
resetWriteState();
|
||||
};
|
||||
}, [disableTerminalTextareas, useHiddenInputOverlay, fitTerminal, fontFamily, fontSize, setupTouchScroll, theme, resetWriteState]);
|
||||
}, [disableTerminalTextareas, fitTerminal, fontFamily, fontSize, setupTouchScroll, theme, resetWriteState, setTerminalCursorBlink, useHiddenInputOverlay]);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -935,10 +1017,20 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
resetWriteState();
|
||||
lastReportedSizeRef.current = null;
|
||||
fitTerminal();
|
||||
if (!useHiddenInputOverlay) {
|
||||
terminal.focus();
|
||||
}, [sessionKey, terminalReadyVersion, fitTerminal, resetWriteState]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!autoFocus) {
|
||||
return;
|
||||
}
|
||||
}, [useHiddenInputOverlay, sessionKey, terminalReadyVersion, fitTerminal, resetWriteState]);
|
||||
|
||||
const terminal = terminalRef.current;
|
||||
if (!terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
focusTerminalInput();
|
||||
}, [autoFocus, focusTerminalInput, sessionKey, terminalReadyVersion]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setupTouchScroll();
|
||||
@@ -984,11 +1076,7 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
ref,
|
||||
(): TerminalController => ({
|
||||
focus: () => {
|
||||
if (useHiddenInputOverlay) {
|
||||
focusHiddenInput();
|
||||
return;
|
||||
}
|
||||
terminalRef.current?.focus();
|
||||
focusTerminalInput();
|
||||
},
|
||||
clear: () => {
|
||||
const terminal = terminalRef.current;
|
||||
@@ -1003,7 +1091,7 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
||||
fitTerminal();
|
||||
},
|
||||
}),
|
||||
[useHiddenInputOverlay, focusHiddenInput, fitTerminal, resetWriteState]
|
||||
[focusTerminalInput, fitTerminal, resetWriteState]
|
||||
);
|
||||
|
||||
const handleHiddenInputBlur = React.useCallback(
|
||||
|
||||
Reference in New Issue
Block a user