chore: remove dead code (59 unused files + ~125 unused exports) (#1835)

* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Serhii Dziupin
2026-06-26 19:27:53 +03:00
committed by GitHub
co-authored by Serhii Dziupin Bohdan Triapitsyn
parent 4a37b9a005
commit 00821700de
324 changed files with 444 additions and 14876 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ import { useDirectorySync, useSessionPermissions, useSessionQuestions, useSessio
import { isFullySyntheticMessage } from '@/lib/messages/synthetic';
import { useCurrentSessionActivity } from './useSessionActivity';
export type AssistantActivity = 'idle' | 'streaming' | 'tooling' | 'cooldown' | 'permission';
type AssistantActivity = 'idle' | 'streaming' | 'tooling' | 'cooldown' | 'permission';
interface WorkingSummary {
activity: AssistantActivity;
-229
View File
@@ -1,229 +0,0 @@
import React from 'react';
const DEFAULT_TIMEOUT_MS = 20000;
const LIFECYCLE_GRACE_MS = 8000;
type MessageStreamPhase = 'streaming' | 'cooldown' | 'completed';
interface MessageStreamLifecycle {
phase: MessageStreamPhase;
startedAt: number;
lastUpdateAt: number;
completedAt?: number;
}
interface MessagePart {
type?: string;
time?: { end?: number };
state?: { status?: string };
text?: string;
content?: string;
}
interface ChatMessageInfo {
id: string;
role: string;
time: { created: number; completed?: number; updated?: number };
animationSettled?: boolean;
}
interface ChatMessageRecord {
info: ChatMessageInfo;
parts: MessagePart[];
}
const hasFinalizedTextPart = (parts: MessagePart[]): boolean => {
return parts.some((part) => {
if (part?.type !== 'text') {
return false;
}
if (!part?.time || typeof part.time.end === 'undefined') {
return false;
}
const content = typeof part.text === 'string' ? part.text : part.content;
return Boolean(content && content.trim().length > 0);
});
};
const getAssistantMessagesAfterLastUser = (messages: ChatMessageRecord[]): ChatMessageRecord[] => {
let lastUserIndex = -1;
for (let i = messages.length - 1; i >= 0; i -= 1) {
if (messages[i]?.info?.role === 'user') {
lastUserIndex = i;
break;
}
}
return messages.filter((message, index) => index > lastUserIndex && message?.info?.role === 'assistant');
};
const buildAssistantActivitySignature = (messages: ChatMessageRecord[]): string => {
return messages
.map((message) => {
const partSignature = (message.parts || [])
.map((part) => {
const type = part?.type || 'unknown';
const finalized = part?.time && typeof part.time.end !== 'undefined' ? '1' : '0';
const status = part?.state?.status || '';
const textLength = typeof part?.text === 'string' ? part.text.length : 0;
const contentLength = typeof part?.content === 'string' ? part.content.length : 0;
return `${type}:${finalized}:${status}:${textLength}:${contentLength}`;
})
.join('|');
const completed = message.info?.time?.completed || '';
const updated = message.info?.time?.updated || '';
return `${message.info?.id || 'unknown'}:${message.parts?.length || 0}:${completed}:${updated}:${partSignature}`;
})
.join('||');
};
interface UseAssistantTypingOptions {
messages: ChatMessageRecord[];
timeoutMs?: number;
messageStreamStates?: Map<string, MessageStreamLifecycle>;
}
interface UseAssistantTypingResult {
isTyping: boolean;
}
export const useAssistantTyping = ({
messages,
timeoutMs = DEFAULT_TIMEOUT_MS,
messageStreamStates,
}: UseAssistantTypingOptions): UseAssistantTypingResult => {
const assistantMessages = React.useMemo(() => getAssistantMessagesAfterLastUser(messages), [messages]);
const hasAssistantActivity = assistantMessages.length > 0;
const hasFinalAssistantText = assistantMessages.some((message) => hasFinalizedTextPart(message.parts));
const assistantHasUnsettledAnimation = assistantMessages.some((message) => {
return message.info.animationSettled !== true;
});
const hasActiveLifecycle = React.useMemo(() => {
if (!messageStreamStates || messageStreamStates.size === 0) {
return false;
}
return assistantMessages.some((message) => {
const lifecycle = messageStreamStates.get(message.info.id);
if (!lifecycle) {
return false;
}
return lifecycle.phase === 'streaming' || lifecycle.phase === 'cooldown';
});
}, [assistantMessages, messageStreamStates]);
const hasRunningTool = React.useMemo(() => {
return assistantMessages.some((message) =>
(message.parts || []).some(
(part) => part?.type === 'tool' && part?.state?.status === 'running'
)
);
}, [assistantMessages]);
const shouldShowBecauseOfLifecycle = hasAssistantActivity && (hasActiveLifecycle || hasRunningTool);
const shouldShowBasedOnContent = assistantHasUnsettledAnimation && hasAssistantActivity && !hasFinalAssistantText;
const [graceUntil, setGraceUntil] = React.useState<number | null>(null);
const previousLifecycleRef = React.useRef<boolean>(false);
React.useEffect(() => {
if (shouldShowBecauseOfLifecycle) {
setGraceUntil(null);
} else if (previousLifecycleRef.current) {
setGraceUntil(Date.now() + LIFECYCLE_GRACE_MS);
}
previousLifecycleRef.current = shouldShowBecauseOfLifecycle;
}, [shouldShowBecauseOfLifecycle]);
React.useEffect(() => {
if (graceUntil === null) {
return undefined;
}
const remaining = graceUntil - Date.now();
if (remaining <= 0) {
setGraceUntil(null);
return undefined;
}
const timer = window.setTimeout(() => {
setGraceUntil(null);
}, remaining);
return () => window.clearTimeout(timer);
}, [graceUntil]);
const withinLifecycleGrace = graceUntil !== null;
const shouldShowIndicator = shouldShowBasedOnContent || shouldShowBecauseOfLifecycle || withinLifecycleGrace;
const signatureRef = React.useRef<string | null>(null);
const [lastActivityAt, setLastActivityAt] = React.useState<number | null>(null);
const [hasTimedOut, setHasTimedOut] = React.useState(false);
React.useEffect(() => {
if (!shouldShowIndicator) {
signatureRef.current = null;
setLastActivityAt(null);
setHasTimedOut(false);
return;
}
const contentSignature = buildAssistantActivitySignature(assistantMessages);
const lifecycleSignature = messageStreamStates
? assistantMessages
.map((message) => {
const lifecycle = messageStreamStates.get(message.info.id);
if (!lifecycle) {
return `${message.info.id}:none`;
}
return `${message.info.id}:${lifecycle.phase}:${lifecycle.lastUpdateAt}:${
lifecycle.completedAt || ''
}`;
})
.join('||')
: '';
const signature = `${contentSignature}::${lifecycleSignature}::${hasRunningTool ? 'tool-running' : ''}::${
graceUntil ?? 'no-grace'
}`;
if (signatureRef.current !== signature) {
signatureRef.current = signature;
setLastActivityAt(Date.now());
setHasTimedOut(false);
}
}, [assistantMessages, shouldShowIndicator, messageStreamStates, hasRunningTool, graceUntil]);
React.useEffect(() => {
if (!shouldShowIndicator) {
return undefined;
}
if (lastActivityAt === null) {
return undefined;
}
const now = Date.now();
const elapsed = now - lastActivityAt;
if (elapsed >= timeoutMs) {
setHasTimedOut(true);
return undefined;
}
const remaining = timeoutMs - elapsed;
const timer = window.setTimeout(() => {
setHasTimedOut(true);
}, remaining);
return () => window.clearTimeout(timer);
}, [shouldShowIndicator, lastActivityAt, timeoutMs]);
const isTyping = shouldShowIndicator && !hasTimedOut;
return React.useMemo(() => ({ isTyping }), [isTyping]);
};
@@ -1,49 +0,0 @@
import React from 'react';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
export const useAvailableTools = () => {
const { tools: toolsAPI } = useRuntimeAPIs();
const [tools, setTools] = React.useState<string[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
let cancelled = false;
const fetchTools = async () => {
setIsLoading(true);
setError(null);
try {
const availableTools = await toolsAPI.getAvailableTools();
if (cancelled) {
return;
}
setTools(availableTools);
} catch (err) {
if (cancelled) {
return;
}
const message = err instanceof Error ? err.message : 'Failed to fetch tools';
console.error('Failed to fetch available tools:', message);
setError(message);
setTools([]);
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
fetchTools();
return () => {
cancelled = true;
};
}, [toolsAPI]);
return { tools, isLoading, error };
};
+1 -1
View File
@@ -4,7 +4,7 @@ import { MessageFreshnessDetector } from '@/lib/messageFreshness';
import { createScrollSpy } from '@/components/chat/lib/scroll/scrollSpy';
import { useViewportStore } from '@/sync/viewport-store';
export type AutoFollowState = 'following' | 'released';
type AutoFollowState = 'following' | 'released';
export type ContentChangeReason = 'text' | 'structural' | 'permission';
-194
View File
@@ -1,194 +0,0 @@
import React from 'react';
import { animate } from 'motion/react';
import { useOptionalDrawer } from '@/contexts/DrawerContext';
type DrawerSwipeOptions = {
edgeSide?: 'left' | 'right';
strictHorizontalIntent?: boolean;
horizontalIntentRatio?: number;
activationDistance?: number;
onlyWhenClosed?: boolean;
};
export function useDrawerSwipe(options: DrawerSwipeOptions = {}) {
const drawer = useOptionalDrawer();
const {
edgeSide,
strictHorizontalIntent = false,
horizontalIntentRatio = 1.35,
activationDistance = 30,
onlyWhenClosed = false,
} = options;
const touchStartXRef = React.useRef(0);
const touchStartYRef = React.useRef(0);
const isHorizontalSwipeRef = React.useRef<boolean | null>(null);
const isDraggingDrawerRef = React.useRef<'left' | 'right' | null>(null);
const handleTouchStart = React.useCallback((e: React.TouchEvent) => {
if (!drawer) return;
touchStartXRef.current = e.touches[0].clientX;
touchStartYRef.current = e.touches[0].clientY;
isHorizontalSwipeRef.current = null;
isDraggingDrawerRef.current = null;
}, [drawer]);
const handleTouchMove = React.useCallback((e: React.TouchEvent) => {
if (!drawer) return;
const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;
const deltaX = currentX - touchStartXRef.current;
const deltaY = currentY - touchStartYRef.current;
if (isHorizontalSwipeRef.current === null) {
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
if (strictHorizontalIntent) {
isHorizontalSwipeRef.current = Math.abs(deltaX) > Math.abs(deltaY) * horizontalIntentRatio;
} else {
isHorizontalSwipeRef.current = Math.abs(deltaX) > Math.abs(deltaY);
}
}
}
if (isHorizontalSwipeRef.current === true) {
if (onlyWhenClosed && (drawer.leftDrawerOpen || drawer.rightDrawerOpen)) {
return;
}
const leftDrawerWidthPx = drawer.leftDrawerWidth.current || window.innerWidth * 0.85;
const rightDrawerWidthPx = drawer.rightDrawerWidth.current || window.innerWidth * 0.85;
if (isDraggingDrawerRef.current === null) {
if (!edgeSide && drawer.leftDrawerOpen && deltaX > 10) {
isDraggingDrawerRef.current = 'left';
} else if (!edgeSide && drawer.rightDrawerOpen && deltaX < -10) {
isDraggingDrawerRef.current = 'right';
} else if (!drawer.leftDrawerOpen && !drawer.rightDrawerOpen) {
if (edgeSide === 'left') {
if (deltaX > activationDistance) {
isDraggingDrawerRef.current = 'left';
}
} else if (edgeSide === 'right') {
if (deltaX < -activationDistance) {
isDraggingDrawerRef.current = 'right';
}
} else if (deltaX > activationDistance) {
isDraggingDrawerRef.current = 'left';
} else if (deltaX < -activationDistance) {
isDraggingDrawerRef.current = 'right';
}
}
}
if (!isDraggingDrawerRef.current) {
return;
}
e.preventDefault();
if (isDraggingDrawerRef.current === 'left') {
if (drawer.leftDrawerOpen) {
const progress = Math.max(0, Math.min(1, deltaX / leftDrawerWidthPx));
drawer.leftDrawerX.set(-leftDrawerWidthPx * (1 - progress));
} else {
const progress = Math.max(0, Math.min(1, deltaX / leftDrawerWidthPx));
drawer.leftDrawerX.set(-leftDrawerWidthPx + (leftDrawerWidthPx * progress));
}
}
if (isDraggingDrawerRef.current === 'right') {
if (drawer.rightDrawerOpen) {
const progress = Math.max(0, Math.min(1, -deltaX / rightDrawerWidthPx));
drawer.rightDrawerX.set(rightDrawerWidthPx * (1 - progress));
} else {
const progress = Math.max(0, Math.min(1, -deltaX / rightDrawerWidthPx));
drawer.rightDrawerX.set(rightDrawerWidthPx - (rightDrawerWidthPx * progress));
}
}
}
}, [activationDistance, drawer, edgeSide, horizontalIntentRatio, onlyWhenClosed, strictHorizontalIntent]);
const handleTouchEnd = React.useCallback((e: React.TouchEvent) => {
if (!drawer) return;
if (isHorizontalSwipeRef.current !== true) return;
const endX = e.changedTouches[0].clientX;
const deltaX = endX - touchStartXRef.current;
const velocityThreshold = 500;
const progressThreshold = 0.3;
const leftDrawerWidthPx = drawer.leftDrawerWidth.current || window.innerWidth * 0.85;
const rightDrawerWidthPx = drawer.rightDrawerWidth.current || window.innerWidth * 0.85;
if (isDraggingDrawerRef.current === 'left') {
const isOpen = drawer.leftDrawerOpen;
const currentX = drawer.leftDrawerX.get();
const progress = isOpen
? 1 - Math.abs(currentX) / leftDrawerWidthPx
: 1 + currentX / leftDrawerWidthPx;
const shouldComplete = progress > progressThreshold || Math.abs(deltaX * 10) > velocityThreshold;
if (shouldComplete) {
const targetX = isOpen ? -leftDrawerWidthPx : 0;
animate(drawer.leftDrawerX, targetX, {
type: 'spring',
stiffness: 400,
damping: 35,
mass: 0.8,
});
drawer.setMobileLeftDrawerOpen(!isOpen);
} else {
const targetX = isOpen ? 0 : -leftDrawerWidthPx;
animate(drawer.leftDrawerX, targetX, {
type: 'spring',
stiffness: 400,
damping: 35,
mass: 0.8,
});
}
isDraggingDrawerRef.current = null;
return;
}
if (isDraggingDrawerRef.current === 'right') {
const isOpen = drawer.rightDrawerOpen;
const currentX = drawer.rightDrawerX.get();
const progress = isOpen
? 1 - Math.abs(currentX) / rightDrawerWidthPx
: 1 - currentX / rightDrawerWidthPx;
const shouldComplete = progress > progressThreshold || Math.abs(deltaX * 10) > velocityThreshold;
if (shouldComplete) {
const targetX = isOpen ? rightDrawerWidthPx : 0;
animate(drawer.rightDrawerX, targetX, {
type: 'spring',
stiffness: 400,
damping: 35,
mass: 0.8,
});
drawer.setRightSidebarOpen(!isOpen);
} else {
const targetX = isOpen ? 0 : rightDrawerWidthPx;
animate(drawer.rightDrawerX, targetX, {
type: 'spring',
stiffness: 400,
damping: 35,
mass: 0.8,
});
}
isDraggingDrawerRef.current = null;
return;
}
isHorizontalSwipeRef.current = null;
}, [drawer]);
return {
handleTouchStart,
handleTouchMove,
handleTouchEnd,
};
}
-130
View File
@@ -1,130 +0,0 @@
import { useEffect, useRef } from 'react';
import { useUIStore } from '@/stores/useUIStore';
interface EdgeSwipeOptions {
edgeThreshold?: number;
minSwipeDistance?: number;
maxSwipeTime?: number;
enabled?: boolean;
}
export const useEdgeSwipe = (options: EdgeSwipeOptions = {}) => {
const {
edgeThreshold = 30,
minSwipeDistance = 50,
maxSwipeTime = 300,
enabled = true,
} = options;
const isMobile = useUIStore((state) => state.isMobile);
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const isSessionSwitcherOpen = useUIStore((state) => state.isSessionSwitcherOpen);
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
const touchEndRef = useRef<{ x: number; y: number; time: number } | null>(null);
useEffect(() => {
if (!enabled || !isMobile) return;
const handleTouchStart = (e: TouchEvent) => {
const touch = e.touches[0];
if (!touch) {
touchStartRef.current = null;
return;
}
const fromLeft = touch.clientX <= edgeThreshold;
if (fromLeft) {
touchStartRef.current = {
x: touch.clientX,
y: touch.clientY,
time: Date.now(),
};
} else {
touchStartRef.current = null;
}
};
const handleTouchMove = (e: TouchEvent) => {
if (!touchStartRef.current) {
return;
}
const touch = e.touches[0];
if (!touch) {
return;
}
const deltaX = touch.clientX - touchStartRef.current.x;
if (deltaX > 10) {
e.preventDefault();
}
};
const handleTouchEnd = (e: TouchEvent) => {
if (!touchStartRef.current) return;
const touch = e.changedTouches[0];
if (!touch) {
touchStartRef.current = null;
touchEndRef.current = null;
return;
}
touchEndRef.current = {
x: touch.clientX,
y: touch.clientY,
time: Date.now(),
};
const { x: startX, y: startY, time: startTime } = touchStartRef.current;
const { x: endX, y: endY, time: endTime } = touchEndRef.current;
const deltaX = endX - startX;
const deltaY = endY - startY;
const deltaTime = endTime - startTime;
const isHorizontal = Math.abs(deltaY) < Math.abs(deltaX);
const isQuick = deltaTime <= maxSwipeTime;
const limitedVertical = Math.abs(deltaY) < minSwipeDistance;
const isValidLeftSwipe =
deltaX >= minSwipeDistance && isHorizontal && isQuick && limitedVertical;
if (isValidLeftSwipe && !isSessionSwitcherOpen) {
setSessionSwitcherOpen(true);
}
touchStartRef.current = null;
touchEndRef.current = null;
};
const handleTouchCancel = () => {
touchStartRef.current = null;
touchEndRef.current = null;
};
document.addEventListener('touchstart', handleTouchStart, { passive: true, capture: true });
document.addEventListener('touchmove', handleTouchMove, { passive: false, capture: true });
document.addEventListener('touchend', handleTouchEnd, { passive: true, capture: true });
document.addEventListener('touchcancel', handleTouchCancel, { passive: true, capture: true });
return () => {
document.removeEventListener('touchstart', handleTouchStart, { capture: true });
document.removeEventListener('touchmove', handleTouchMove, { capture: true });
document.removeEventListener('touchend', handleTouchEnd, { capture: true });
document.removeEventListener('touchcancel', handleTouchCancel, { capture: true });
};
}, [
enabled,
isMobile,
edgeThreshold,
minSwipeDistance,
maxSwipeTime,
setSessionSwitcherOpen,
isSessionSwitcherOpen,
]);
return null;
};
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react';
export type GitmojiEntry = {
type GitmojiEntry = {
emoji: string;
code: string;
description: string;
-102
View File
@@ -1,102 +0,0 @@
import { useRef, useCallback, useEffect } from 'react';
type LongPressOptions = {
delay?: number;
onLongPress: () => void;
onTap?: () => void;
enableHaptic?: boolean;
};
export function useLongPress({
delay = 500,
onLongPress,
onTap,
enableHaptic = true,
}: LongPressOptions) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isLongPressRef = useRef(false);
const startPosRef = useRef<{ x: number; y: number } | null>(null);
const clear = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
startPosRef.current = null;
}, []);
const onPointerDown = useCallback((e: React.PointerEvent | React.TouchEvent) => {
clear();
isLongPressRef.current = false;
// Store start position to detect movement
const clientX = 'touches' in e ? e.touches[0].clientX : (e as React.PointerEvent).clientX;
const clientY = 'touches' in e ? e.touches[0].clientY : (e as React.PointerEvent).clientY;
startPosRef.current = { x: clientX, y: clientY };
timerRef.current = setTimeout(() => {
isLongPressRef.current = true;
if (enableHaptic && typeof navigator !== 'undefined' && navigator.vibrate) {
try {
navigator.vibrate(15);
} catch {
// Ignore vibration errors
}
}
onLongPress();
}, delay);
}, [clear, delay, onLongPress, enableHaptic]);
const onPointerMove = useCallback((e: React.PointerEvent | React.TouchEvent) => {
if (!startPosRef.current || !timerRef.current) return;
const clientX = 'touches' in e ? e.touches[0].clientX : (e as React.PointerEvent).clientX;
const clientY = 'touches' in e ? e.touches[0].clientY : (e as React.PointerEvent).clientY;
// If moved more than 10px, cancel long press
const dx = Math.abs(clientX - startPosRef.current.x);
const dy = Math.abs(clientY - startPosRef.current.y);
if (dx > 10 || dy > 10) {
clear();
}
}, [clear]);
const onPointerUp = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
if (!isLongPressRef.current && onTap) {
// Only trigger tap if we didn't drag too far (checked in move)
// and didn't trigger long press
onTap();
}
}
clear();
}, [clear, onTap]);
const onPointerLeave = useCallback(() => {
clear();
}, [clear]);
const onContextMenu = useCallback((e: React.MouseEvent) => {
// Prevent default context menu on long press
if (isLongPressRef.current) {
e.preventDefault();
}
}, []);
useEffect(() => clear, [clear]);
return {
onPointerDown,
onPointerMove,
onPointerUp,
onPointerLeave,
onTouchStart: onPointerDown, // Add touch handlers for better mobile support
onTouchMove: onPointerMove,
onTouchEnd: onPointerUp,
onTouchCancel: clear,
onContextMenu,
};
}
+1 -1
View File
@@ -63,7 +63,7 @@ const resolveProviderLogoSrc = (providerId: string | null | undefined): string |
return remoteResolvedId ? `https://models.dev/logos/${remoteResolvedId}.svg` : null;
};
export const preloadProviderLogo = (providerId: string | null | undefined): void => {
const preloadProviderLogo = (providerId: string | null | undefined): void => {
if (typeof Image === 'undefined') return;
const src = resolveProviderLogoSrc(providerId);
if (!src || PRELOADED_LOGO_SRCS.has(src)) return;
-99
View File
@@ -246,102 +246,3 @@ export function useRouter(): void {
};
}, [applyRoute, isVSCode, setActiveMainTab, setSettingsDialogOpen]);
}
/**
* Programmatically navigate to a route.
* Can be used from outside React components.
*/
export function navigateToRoute(route: Partial<RouteState>): void {
if (typeof window === 'undefined') {
return;
}
// Check VS Code context
const win = window as { __VSCODE_CONFIG__?: unknown };
if (win.__VSCODE_CONFIG__ !== undefined) {
// In VS Code, just apply state changes directly
if (route.sessionId) {
void useSessionUIStore.getState().setCurrentSession(route.sessionId);
}
if (route.settingsPath) {
useUIStore.getState().setSettingsPage(resolveSettingsSlug(route.settingsPath));
useUIStore.getState().setSettingsDialogOpen(true);
} else if (route.tab) {
useUIStore.getState().setActiveMainTab(route.tab);
}
if (route.diffFile) {
useUIStore.getState().navigateToDiff(route.diffFile);
}
return;
}
// Build URL and navigate
const params = new URLSearchParams();
if (route.sessionId) {
params.set('session', route.sessionId);
}
if (route.settingsPath) {
params.set('settings', route.settingsPath);
} else if (route.tab && route.tab !== 'chat') {
if (useUIStore.getState().isSettingsDialogOpen) {
useUIStore.getState().setSettingsDialogOpen(false);
}
params.set('tab', route.tab);
}
if (route.diffFile) {
params.set('file', route.diffFile);
}
const search = params.toString();
const url = search ? `${window.location.pathname}?${search}` : window.location.pathname;
window.history.pushState({ route }, '', url);
// Also apply to state
if (route.sessionId) {
void useSessionUIStore.getState().setCurrentSession(route.sessionId);
}
if (route.settingsPath) {
useUIStore.getState().setSettingsPage(resolveSettingsSlug(route.settingsPath));
useUIStore.getState().setSettingsDialogOpen(true);
} else if (route.tab) {
useUIStore.getState().setActiveMainTab(route.tab);
}
if (route.diffFile) {
useUIStore.getState().navigateToDiff(route.diffFile);
}
}
/**
* Get a shareable URL for the current state.
*/
export function getShareableURL(): string {
if (typeof window === 'undefined') {
return '/';
}
const sessionState = useSessionUIStore.getState();
const uiState = useUIStore.getState();
const params = new URLSearchParams();
if (sessionState.currentSessionId) {
params.set('session', sessionState.currentSessionId);
}
if (uiState.isSettingsDialogOpen) {
params.set('settings', uiState.settingsPage || 'home');
} else if (uiState.activeMainTab !== 'chat') {
params.set('tab', uiState.activeMainTab);
}
if (uiState.activeMainTab === 'diff' && uiState.pendingDiffFile) {
params.set('file', uiState.pendingDiffFile);
}
const search = params.toString();
const base = `${window.location.origin}${window.location.pathname}`;
return search ? `${base}?${search}` : base;
}
+1 -1
View File
@@ -10,7 +10,7 @@ export const useRuntimeAPIs = (): RuntimeAPIs => {
return apis;
};
export const useRuntimeAPI = <TValue,>(selector: RuntimeAPISelector<TValue>): TValue => {
const useRuntimeAPI = <TValue,>(selector: RuntimeAPISelector<TValue>): TValue => {
const apis = useRuntimeAPIs();
return selector(apis);
};
+1 -1
View File
@@ -100,7 +100,7 @@ export interface UseSayTTSReturn {
unlockAudio: () => Promise<void>;
}
export interface SpeakOptions {
interface SpeakOptions {
/** Voice to use (defaults to Samantha) */
voice?: string;
/** Speech rate in words per minute (defaults to 200) */
+1 -1
View File
@@ -84,7 +84,7 @@ export interface UseServerTTSReturn {
unlockAudio: () => Promise<void>;
}
export interface SpeakOptions {
interface SpeakOptions {
/** Voice to use (defaults to coral) */
voice?: string;
/** Model to use (defaults to gpt-4o-mini-tts) */
+2 -2
View File
@@ -3,7 +3,7 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionStatus, useSessionMessages, useSessionPermissions, useSessionQuestions } from '@/sync/sync-context';
// Mirrors OpenCode SessionStatus: busy|retry|idle.
export type SessionActivityPhase = 'idle' | 'busy' | 'retry';
type SessionActivityPhase = 'idle' | 'busy' | 'retry';
export interface SessionActivityResult {
phase: SessionActivityPhase;
@@ -27,7 +27,7 @@ const IDLE_RESULT: SessionActivityResult = {
* question indicator takes priority, and the send button must stay available so
* the user can supersede the prompt with a new message).
*/
export function useSessionActivity(sessionId: string | null | undefined, directory?: string): SessionActivityResult {
function useSessionActivity(sessionId: string | null | undefined, directory?: string): SessionActivityResult {
const status = useSessionStatus(sessionId ?? '', directory);
const messages = useSessionMessages(sessionId ?? '', directory);
const permissions = useSessionPermissions(sessionId ?? '', directory);
@@ -22,7 +22,7 @@ type BuildAutoDeleteCandidatesOptions = {
now?: number;
};
export const buildAutoDeleteCandidates = ({
const buildAutoDeleteCandidates = ({
sessions,
currentSessionId,
cutoffDays,
-141
View File
@@ -1,141 +0,0 @@
import { useState, useRef, useEffect, useMemo } from "react"
type StageConfig = {
/** How many messages to show on first paint */
init: number
/** How many to add per animation frame */
batch: number
}
type UseTimelineStagingInput<T> = {
/** Key that changes when session switches */
sessionKey: string
/** All messages (sorted) */
messages: T[]
/** Config for staging behavior */
config?: StageConfig
}
type UseTimelineStagingResult<T> = {
/** The subset of messages that should be rendered */
stagedMessages: T[]
/** Whether staging is still in progress */
isStaging: boolean
/** Force the current session timeline to render fully now */
completeNow: () => boolean
}
const DEFAULT_CONFIG: StageConfig = { init: 1, batch: 3 }
/**
* Defer-mounts small timeline windows so revealing older turns does not
* block first paint with a large DOM mount.
*
* Once staging completes for a session it never re-stages — backfill and
* new messages render immediately.
*
* Defers mounting older turns so first paint isn't blocked by large DOM.
*/
export function useTimelineStaging<T>(
input: UseTimelineStagingInput<T>,
): UseTimelineStagingResult<T> {
const config = input.config ?? DEFAULT_CONFIG
const { sessionKey, messages } = input
const [stagedCount, setStagedCount] = useState(() => messages.length)
const completedSessions = useRef(new Set<string>())
const activeSession = useRef("")
const frameRef = useRef<number | null>(null)
const completeNow = () => {
if (!sessionKey) {
return false
}
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
activeSession.current = ""
completedSessions.current.add(sessionKey)
const total = messages.length
let changed = false
setStagedCount((previous) => {
if (previous === total) {
return previous
}
changed = true
return total
})
return changed
}
useEffect(() => {
// Cancel any pending animation frame
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
const total = messages.length
// If already completed for this session, show all immediately
if (completedSessions.current.has(sessionKey)) {
setStagedCount(total)
return
}
// Small message list — no staging needed
if (total <= config.init) {
setStagedCount(total)
completedSessions.current.add(sessionKey)
return
}
// Start staging
activeSession.current = sessionKey
let count = Math.min(total, config.init)
setStagedCount(count)
const step = () => {
// Session changed mid-staging — bail
if (activeSession.current !== sessionKey) {
frameRef.current = null
return
}
count = Math.min(messages.length, count + config.batch)
setStagedCount(count)
if (count >= messages.length) {
completedSessions.current.add(sessionKey)
activeSession.current = ""
frameRef.current = null
return
}
frameRef.current = requestAnimationFrame(step)
}
frameRef.current = requestAnimationFrame(step)
return () => {
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
}
}, [sessionKey, messages.length, config.init, config.batch])
const stagedMessages = useMemo(() => {
if (stagedCount >= messages.length) return messages
return messages.slice(Math.max(0, messages.length - stagedCount))
}, [messages, stagedCount])
const isStaging = activeSession.current === sessionKey &&
!completedSessions.current.has(sessionKey)
return { stagedMessages, isStaging, completeNow }
}
@@ -1,35 +0,0 @@
import { describe, expect, test } from 'bun:test';
import { getVisualViewportState } from './useVisualViewport';
const withWindow = (value: { innerHeight: number; visualViewport?: { height: number } | null }, run: () => void) => {
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
Object.defineProperty(globalThis, 'window', {
configurable: true,
value,
});
try {
run();
} finally {
if (originalWindow) {
Object.defineProperty(globalThis, 'window', originalWindow);
} else {
Reflect.deleteProperty(globalThis, 'window');
}
}
};
describe('getVisualViewportState', () => {
test('falls back to innerHeight when visualViewport is unavailable', () => {
withWindow({ innerHeight: 812 }, () => {
expect(getVisualViewportState()).toEqual({ height: 812, keyboardHeight: 0 });
});
});
test('derives keyboard height from visualViewport height', () => {
withWindow({ innerHeight: 812, visualViewport: { height: 512 } }, () => {
expect(getVisualViewportState()).toEqual({ height: 512, keyboardHeight: 300 });
});
});
});
@@ -1,65 +0,0 @@
import React from 'react';
export interface VisualViewportState {
height: number;
keyboardHeight: number;
}
export const getVisualViewportState = (): VisualViewportState => {
if (typeof window === 'undefined') {
return { height: 0, keyboardHeight: 0 };
}
const height = window.visualViewport?.height ?? window.innerHeight;
const keyboardHeight = window.visualViewport
? Math.max(0, window.innerHeight - height)
: 0;
return { height, keyboardHeight };
};
export const useVisualViewport = (): VisualViewportState => {
const [state, setState] = React.useState<VisualViewportState>(getVisualViewportState);
const rafIdRef = React.useRef<number | null>(null);
React.useEffect(() => {
if (typeof window === 'undefined') return;
const visualViewport = window.visualViewport;
const handleChange = () => {
if (rafIdRef.current !== null) return;
rafIdRef.current = requestAnimationFrame(() => {
rafIdRef.current = null;
const nextState = getVisualViewportState();
setState((prev) => {
if (prev.height === nextState.height && prev.keyboardHeight === nextState.keyboardHeight) return prev;
return nextState;
});
});
};
if (visualViewport) {
visualViewport.addEventListener('resize', handleChange);
visualViewport.addEventListener('scroll', handleChange, { passive: true });
} else {
window.addEventListener('resize', handleChange);
}
handleChange();
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
}
if (visualViewport) {
visualViewport.removeEventListener('resize', handleChange);
visualViewport.removeEventListener('scroll', handleChange);
} else {
window.removeEventListener('resize', handleChange);
}
};
}, []);
return state;
};