refactor: unify workspace tab management and add runtime metrics

This commit is contained in:
Mohamed Boudra
2026-03-05 14:21:16 +07:00
parent 6dc49ccbc7
commit 8371ea0c7d
32 changed files with 1064 additions and 461 deletions
@@ -1,6 +1,6 @@
import { View, Pressable, Text, ActivityIndicator, Platform } from 'react-native'
import { useState, useEffect, useRef, useCallback } from 'react'
import { StyleSheet, useUnistyles } from 'react-native-unistyles'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { ArrowUp, Square, Pencil, AudioLines } from 'lucide-react-native'
import Animated from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -119,6 +119,10 @@ export function AgentInputArea({
const setAgentStreamHead = useSessionStore((state) => state.setAgentStreamHead)
const [internalInput, setInternalInput] = useState('')
const isDesktopWebBreakpoint =
Platform.OS === 'web' &&
UnistylesRuntime.breakpoint !== 'xs' &&
UnistylesRuntime.breakpoint !== 'sm'
const userInput = value ?? internalInput
const setUserInput = onChangeText ?? setInternalInput
const [cursorIndex, setCursorIndex] = useState(0)
@@ -775,7 +779,8 @@ export function AgentInputArea({
client={client}
isReadyForDictation={isDictationReady}
placeholder="Message agent..."
autoFocus={autoFocus}
autoFocus={autoFocus && isDesktopWebBreakpoint}
autoFocusKey={`${serverId}:${agentId}`}
disabled={isSubmitLoading}
isScreenFocused={isScreenFocused}
leftContent={leftContent}
@@ -440,14 +440,14 @@ const styles = StyleSheet.create((theme) => ({
top: 0,
right: 0,
bottom: 0,
backgroundColor: theme.colors.surface0,
backgroundColor: theme.colors.surfaceSidebar,
overflow: "hidden",
},
desktopSidebar: {
position: "relative",
borderLeftWidth: 1,
borderLeftColor: theme.colors.border,
backgroundColor: theme.colors.surface0,
backgroundColor: theme.colors.surfaceSidebar,
},
resizeHandle: {
position: "absolute",
@@ -948,7 +948,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd }: GitDiffPaneProps) {
if (isStatusLoading) {
bodyContent = (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" />
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
<Text style={styles.loadingText}>Checking repository...</Text>
</View>
);
@@ -967,8 +967,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd }: GitDiffPaneProps) {
} else if (isDiffLoading) {
bodyContent = (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" />
<Text style={styles.loadingText}>Loading changes...</Text>
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
</View>
);
} else if (diffErrorMessage) {
@@ -1234,9 +1233,6 @@ export function GitDiffPane({ serverId, workspaceId, cwd }: GitDiffPaneProps) {
<Text style={styles.branchLabel} testID="changes-branch" numberOfLines={1}>
{branchLabel}
</Text>
{isStatusFetching && (
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
)}
</View>
{isGit ? (
<View style={styles.headerRight}>
+2 -2
View File
@@ -560,7 +560,7 @@ const styles = StyleSheet.create((theme) => ({
top: 0,
left: 0,
bottom: 0,
backgroundColor: theme.colors.surface0,
backgroundColor: theme.colors.surfaceSidebar,
overflow: 'hidden',
},
sidebarContent: {
@@ -571,7 +571,7 @@ const styles = StyleSheet.create((theme) => ({
desktopSidebar: {
borderRightWidth: 1,
borderRightColor: theme.colors.border,
backgroundColor: theme.colors.surface0,
backgroundColor: theme.colors.surfaceSidebar,
},
sidebarHeader: {
height: {
+14 -6
View File
@@ -30,6 +30,7 @@ import {
} from '@/utils/image-attachments-from-files'
import type { AttachmentMetadata } from '@/attachments/types'
import { useAttachmentPreviewUrl } from '@/attachments/use-attachment-preview-url'
import { focusWithRetries } from '@/utils/web-focus'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Shortcut } from '@/components/ui/shortcut'
import type { MessageInputKeyboardActionKind } from '@/keyboard/actions'
@@ -62,6 +63,7 @@ export interface MessageInputProps {
isReadyForDictation?: boolean
placeholder?: string
autoFocus?: boolean
autoFocusKey?: string
disabled?: boolean
/** True when the containing screen is focused (React Navigation). Used to disable global hotkeys and cancel dictation when unfocused. */
isScreenFocused?: boolean
@@ -137,6 +139,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
isReadyForDictation,
placeholder = 'Message...',
autoFocus = false,
autoFocusKey,
disabled = false,
isScreenFocused = true,
leftContent,
@@ -231,15 +234,20 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
valueRef.current = value
}, [value])
// Autofocus on web when autoFocus prop is true
// Autofocus on web when autoFocus is true, and re-run when focus key changes.
useEffect(() => {
if (!IS_WEB || !autoFocus) return
// Use requestAnimationFrame to ensure DOM is ready
const rafId = requestAnimationFrame(() => {
textInputRef.current?.focus()
return focusWithRetries({
focus: () => textInputRef.current?.focus(),
isFocused: () => {
const current = textInputRef.current as (TextInput & { getNativeRef?: () => unknown }) | null
const native = typeof current?.getNativeRef === 'function' ? current.getNativeRef() : current
const element = native instanceof HTMLElement ? native : null
const active = typeof document !== 'undefined' ? document.activeElement : null
return Boolean(element) && active === element
},
})
return () => cancelAnimationFrame(rafId)
}, [autoFocus])
}, [autoFocus, autoFocusKey])
const handleDictationTranscript = useCallback(
(text: string, _meta: { requestId: string }) => {
@@ -1181,7 +1181,7 @@ const styles = StyleSheet.create((theme) => ({
marginBottom: theme.spacing[1],
},
workspaceListContainer: {
marginLeft: theme.spacing[4],
marginLeft: theme.spacing[2],
},
emptyText: {
color: theme.colors.foregroundMuted,