Switch to double quotes and reformat codebase with Biome

This commit is contained in:
Mohamed Boudra
2026-03-21 20:22:15 +07:00
parent ab40786882
commit f6579dbeb4
805 changed files with 38132 additions and 40175 deletions
@@ -25,7 +25,9 @@ type UseWorkspaceTabLayoutResult = {
layout: WorkspaceTabLayoutResult;
};
export function useWorkspaceTabLayout(input: UseWorkspaceTabLayoutInput): UseWorkspaceTabLayoutResult {
export function useWorkspaceTabLayout(
input: UseWorkspaceTabLayoutInput,
): UseWorkspaceTabLayoutResult {
const { width: viewportWidth } = useWindowDimensions();
const resolvedViewportWidth =
typeof input.viewportWidthOverride === "number" && input.viewportWidthOverride > 0
@@ -39,7 +41,7 @@ export function useWorkspaceTabLayout(input: UseWorkspaceTabLayoutInput): UseWor
tabLabelLengths: input.tabLabelLengths,
metrics: input.metrics,
}),
[input.metrics, input.tabLabelLengths, resolvedViewportWidth]
[input.metrics, input.tabLabelLengths, resolvedViewportWidth],
);
return {
@@ -98,7 +98,7 @@ describe("workspace agent visibility", () => {
agentsHydrated: true,
knownAgentIds,
activeAgentIds,
})
}),
).toBe(true);
});
@@ -112,7 +112,7 @@ describe("workspace agent visibility", () => {
agentsHydrated: true,
knownAgentIds,
activeAgentIds,
})
}),
).toBe(false);
});
@@ -123,7 +123,7 @@ describe("workspace agent visibility", () => {
agentsHydrated: true,
knownAgentIds: new Set<string>(),
activeAgentIds: new Set<string>(),
})
}),
).toBe(true);
});
@@ -42,7 +42,9 @@ export function workspaceAgentVisibilityEqual(
a: WorkspaceAgentVisibility,
b: WorkspaceAgentVisibility,
): boolean {
return setsEqual(a.activeAgentIds, b.activeAgentIds) && setsEqual(a.knownAgentIds, b.knownAgentIds);
return (
setsEqual(a.activeAgentIds, b.activeAgentIds) && setsEqual(a.knownAgentIds, b.knownAgentIds)
);
}
function setsEqual(a: Set<string>, b: Set<string>): boolean {
@@ -54,21 +54,21 @@ describe("workspace bulk close helpers", () => {
makeAgentTab("a2"),
makeTerminalTab("t1"),
makeFileTab("/repo/README.md"),
])
]),
);
expect(message).toBe(
"This will archive 2 agent(s), close 1 terminal(s), and close 1 tab(s). Any running process in a closed terminal will be stopped immediately."
"This will archive 2 agent(s), close 1 terminal(s), and close 1 tab(s). Any running process in a closed terminal will be stopped immediately.",
);
});
it("keeps terminal-only confirmations explicit about stopping running processes", () => {
const message = buildBulkCloseConfirmationMessage(
classifyBulkClosableTabs([makeTerminalTab("t1")])
classifyBulkClosableTabs([makeTerminalTab("t1")]),
);
expect(message).toBe(
"This will close 1 terminal(s). Any running process in a closed terminal will be stopped immediately."
"This will close 1 terminal(s). Any running process in a closed terminal will be stopped immediately.",
);
});
});
@@ -1,5 +1,13 @@
import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from "react";
import { ActivityIndicator, Platform, Pressable, ScrollView, Text, View, type LayoutChangeEvent } from "react-native";
import {
ActivityIndicator,
Platform,
Pressable,
ScrollView,
Text,
View,
type LayoutChangeEvent,
} from "react-native";
import { Columns2, Rows2, SquarePen, SquareTerminal, X } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { SortableInlineList } from "@/components/sortable-inline-list";
@@ -144,9 +152,7 @@ function TabChip({
enabledOnMobile={false}
style={({ hovered, pressed }) => [
styles.tab,
Platform.OS === "web" &&
isDragging &&
({ cursor: "grabbing" } as const),
Platform.OS === "web" && isDragging && ({ cursor: "grabbing" } as const),
{
minWidth: resolvedTabWidth,
width: resolvedTabWidth,
@@ -171,10 +177,7 @@ function TabChip({
>
{isActive && (
<View
style={[
styles.tabFocusIndicator,
!isFocused && styles.tabFocusIndicatorUnfocused,
]}
style={[styles.tabFocusIndicator, !isFocused && styles.tabFocusIndicatorUnfocused]}
/>
)}
<View style={styles.tabHandle}>
@@ -236,12 +239,16 @@ function TabChip({
isClosingTab ? (
<ActivityIndicator
size={12}
color={hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted}
color={
hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted
}
/>
) : (
<X
size={12}
color={hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted}
color={
hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted
}
/>
)
}
@@ -268,7 +275,7 @@ function TabChip({
>
{entry.label}
</ContextMenuItem>
)
),
)}
</ContextMenuContent>
</ContextMenu>
@@ -326,7 +333,7 @@ export function WorkspaceDesktopTabsRow({
estimatedCharWidth: 7,
closeButtonWidth: 22,
}),
[tabsActionsWidth, theme.spacing]
[tabsActionsWidth, theme.spacing],
);
const tabLabelLengths = useMemo(
@@ -335,7 +342,7 @@ export function WorkspaceDesktopTabsRow({
const label = getFallbackTabLabel(tab.tab);
return label.length;
}),
[tabs]
[tabs],
);
const { layout } = useWorkspaceTabLayout({
@@ -356,7 +363,9 @@ export function WorkspaceDesktopTabsRow({
testID="workspace-tabs-scroll"
style={[
styles.tabsScroll,
layout.requiresHorizontalScrollFallback ? styles.tabsScrollOverflow : styles.tabsScrollFitContent,
layout.requiresHorizontalScrollFallback
? styles.tabsScrollOverflow
: styles.tabsScrollFitContent,
]}
contentContainerStyle={styles.tabsContent}
showsHorizontalScrollIndicator={false}
@@ -422,7 +431,7 @@ export function WorkspaceDesktopTabsRow({
</ScrollView>
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
<TooltipTrigger
testID="workspace-new-agent-tab"
onPress={() =>
onSelectNewTabOption({
@@ -569,7 +578,7 @@ function ResolvedDesktopTabChip({
onCopyAgentId,
onCopyResumeCommand,
tabCount,
]
],
);
return (
@@ -1,21 +1,24 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { Keyboard, Platform, ScrollView, Text, View } from 'react-native'
import { StyleSheet } from 'react-native-unistyles'
import { AgentInputArea } from '@/components/agent-input-area'
import { FileDropZone } from '@/components/file-drop-zone'
import { AgentStreamView } from '@/components/agent-stream-view'
import type { ImageAttachment } from '@/components/message-input'
import { useAgentFormState } from '@/hooks/use-agent-form-state'
import { useAgentInputDraft } from '@/hooks/use-agent-input-draft'
import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow'
import { useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
import { buildDraftStoreKey } from '@/stores/draft-keys'
import type { Agent } from '@/stores/session-store'
import { encodeImages } from '@/utils/encode-images'
import type { AgentCapabilityFlags, AgentSessionConfig } from '@server/server/agent/agent-sdk-types'
import type { AgentSnapshotPayload } from '@server/shared/messages'
import { useCallback, useEffect, useMemo, useRef } from "react";
import { Keyboard, Platform, ScrollView, Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { AgentInputArea } from "@/components/agent-input-area";
import { FileDropZone } from "@/components/file-drop-zone";
import { AgentStreamView } from "@/components/agent-stream-view";
import type { ImageAttachment } from "@/components/message-input";
import { useAgentFormState } from "@/hooks/use-agent-form-state";
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
import { useDraftAgentCreateFlow } from "@/hooks/use-draft-agent-create-flow";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import type { Agent } from "@/stores/session-store";
import { encodeImages } from "@/utils/encode-images";
import type {
AgentCapabilityFlags,
AgentSessionConfig,
} from "@server/server/agent/agent-sdk-types";
import type { AgentSnapshotPayload } from "@server/shared/messages";
const EMPTY_PENDING_PERMISSIONS = new Map()
const EMPTY_PENDING_PERMISSIONS = new Map();
const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true,
supportsSessionPersistence: false,
@@ -23,16 +26,16 @@ const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
}
};
type WorkspaceDraftAgentTabProps = {
serverId: string
workspaceId: string
tabId: string
draftId: string
onCreated: (snapshot: AgentSnapshotPayload) => void
onOpenWorkspaceFile: (input: { filePath: string }) => void
}
serverId: string;
workspaceId: string;
tabId: string;
draftId: string;
onCreated: (snapshot: AgentSnapshotPayload) => void;
onOpenWorkspaceFile: (input: { filePath: string }) => void;
};
export function WorkspaceDraftAgentTab({
serverId,
@@ -42,16 +45,16 @@ export function WorkspaceDraftAgentTab({
onCreated,
onOpenWorkspaceFile,
}: WorkspaceDraftAgentTabProps) {
const client = useHostRuntimeClient(serverId)
const isConnected = useHostRuntimeIsConnected(serverId)
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null)
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const draftInput = useAgentInputDraft(
buildDraftStoreKey({
serverId,
agentId: tabId,
draftId,
})
)
}),
);
const {
selectedProvider,
@@ -79,15 +82,15 @@ export function WorkspaceDraftAgentTab({
isVisible: true,
isCreateFlow: true,
onlineServerIds: isConnected ? [serverId] : [],
})
});
// Lock working directory to workspace.
useEffect(() => {
if (workingDir.trim() === workspaceId.trim()) {
return
return;
}
setWorkingDir(workspaceId)
}, [setWorkingDir, workingDir, workspaceId])
setWorkingDir(workspaceId);
}, [setWorkingDir, workingDir, workspaceId]);
const {
formErrorMessage,
@@ -100,33 +103,33 @@ export function WorkspaceDraftAgentTab({
getPendingServerId: () => serverId,
validateBeforeSubmit: ({ text }) => {
if (!text.trim()) {
return 'Initial prompt is required'
return "Initial prompt is required";
}
if (providerDefinitions.length === 0) {
return 'No available providers on the selected host'
return "No available providers on the selected host";
}
if (!client) {
return 'Host is not connected'
return "Host is not connected";
}
return null
return null;
},
onBeforeSubmit: () => {
void persistFormPreferences()
if (Platform.OS === 'web') {
;(document.activeElement as HTMLElement | null)?.blur?.()
void persistFormPreferences();
if (Platform.OS === "web") {
(document.activeElement as HTMLElement | null)?.blur?.();
}
Keyboard.dismiss()
Keyboard.dismiss();
},
buildDraftAgent: (attempt) => {
const now = attempt.timestamp
const model = selectedModel.trim() || null
const thinkingOptionId = selectedThinkingOptionId.trim() || null
const modeId = modeOptions.length > 0 && selectedMode !== '' ? selectedMode : null
const now = attempt.timestamp;
const model = selectedModel.trim() || null;
const thinkingOptionId = selectedThinkingOptionId.trim() || null;
const modeId = modeOptions.length > 0 && selectedMode !== "" ? selectedMode : null;
return {
serverId,
id: tabId,
provider: selectedProvider,
status: 'running',
status: "running",
createdAt: now,
updatedAt: now,
lastUserMessageAt: now,
@@ -137,57 +140,57 @@ export function WorkspaceDraftAgentTab({
pendingPermissions: [],
persistence: null,
runtimeInfo: { provider: selectedProvider, sessionId: null, model, modeId },
title: 'Agent',
title: "Agent",
cwd: workspaceId,
model,
thinkingOptionId,
labels: {},
}
};
},
createRequest: async ({ attempt, text, images }) => {
if (!client) {
throw new Error('Host is not connected')
throw new Error("Host is not connected");
}
const modeId = modeOptions.length > 0 && selectedMode !== '' ? selectedMode : undefined
const trimmedModel = selectedModel.trim()
const trimmedThinkingOptionId = selectedThinkingOptionId.trim()
const modeId = modeOptions.length > 0 && selectedMode !== "" ? selectedMode : undefined;
const trimmedModel = selectedModel.trim();
const trimmedThinkingOptionId = selectedThinkingOptionId.trim();
const config: AgentSessionConfig = {
provider: selectedProvider,
cwd: workspaceId,
...(modeId ? { modeId } : {}),
...(trimmedModel ? { model: trimmedModel } : {}),
...(trimmedThinkingOptionId ? { thinkingOptionId: trimmedThinkingOptionId } : {}),
}
};
const imagesData = await encodeImages(images)
const imagesData = await encodeImages(images);
const result = await client.createAgent({
config,
initialPrompt: text,
clientMessageId: attempt.clientMessageId,
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
})
});
return {
agentId: result.id,
result,
}
};
},
onCreateSuccess: ({ result }) => {
onCreated(result)
onCreated(result);
},
})
});
const draftCommandConfig = useMemo(() => {
return {
provider: selectedProvider,
cwd: workspaceId,
...(modeOptions.length > 0 && selectedMode !== '' ? { modeId: selectedMode } : {}),
...(modeOptions.length > 0 && selectedMode !== "" ? { modeId: selectedMode } : {}),
...(selectedModel.trim() ? { model: selectedModel.trim() } : {}),
...(selectedThinkingOptionId.trim()
? { thinkingOptionId: selectedThinkingOptionId.trim() }
: {}),
}
};
}, [
modeOptions.length,
selectedMode,
@@ -195,15 +198,15 @@ export function WorkspaceDraftAgentTab({
selectedProvider,
selectedThinkingOptionId,
workspaceId,
])
]);
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
addImagesRef.current?.(files)
}, [])
addImagesRef.current?.(files);
}, []);
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
addImagesRef.current = addImages
}, [])
addImagesRef.current = addImages;
}, []);
return (
<FileDropZone onFilesDropped={handleFilesDropped}>
@@ -221,7 +224,10 @@ export function WorkspaceDraftAgentTab({
/>
</View>
) : (
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.configScrollContent}
>
<View style={styles.configSection}>
{formErrorMessage ? (
<View style={styles.errorContainer}>
@@ -271,13 +277,13 @@ export function WorkspaceDraftAgentTab({
</View>
</View>
</FileDropZone>
)
);
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
width: '100%',
width: "100%",
backgroundColor: theme.colors.surface0,
},
contentContainer: {
@@ -298,7 +304,7 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[3],
},
inputAreaWrapper: {
width: '100%',
width: "100%",
backgroundColor: theme.colors.surface0,
},
errorContainer: {
@@ -313,4 +319,4 @@ const styles = StyleSheet.create((theme) => ({
errorText: {
color: theme.colors.destructive,
},
}))
}));
@@ -1,12 +1,6 @@
import { useMemo } from "react";
import { useUnistyles } from "react-native-unistyles";
import {
Archive,
GitCommitHorizontal,
GitMerge,
RefreshCcw,
Upload,
} from "lucide-react-native";
import { Archive, GitCommitHorizontal, GitMerge, RefreshCcw, Upload } from "lucide-react-native";
import { GitHubIcon } from "@/components/icons/github-icon";
import { GitActionsSplitButton } from "@/components/git-actions-split-button";
import { useGitActions } from "@/hooks/use-git-actions";
@@ -19,15 +13,18 @@ interface WorkspaceGitActionsProps {
export function WorkspaceGitActions({ serverId, cwd }: WorkspaceGitActionsProps) {
const { theme } = useUnistyles();
const icons = useMemo(() => ({
commit: <GitCommitHorizontal size={16} color={theme.colors.foregroundMuted} />,
push: <Upload size={16} color={theme.colors.foregroundMuted} />,
viewPr: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
createPr: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
merge: <GitMerge size={16} color={theme.colors.foregroundMuted} />,
mergeFromBase: <RefreshCcw size={16} color={theme.colors.foregroundMuted} />,
archive: <Archive size={16} color={theme.colors.foregroundMuted} />,
}), [theme.colors.foregroundMuted]);
const icons = useMemo(
() => ({
commit: <GitCommitHorizontal size={16} color={theme.colors.foregroundMuted} />,
push: <Upload size={16} color={theme.colors.foregroundMuted} />,
viewPr: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
createPr: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
merge: <GitMerge size={16} color={theme.colors.foregroundMuted} />,
mergeFromBase: <RefreshCcw size={16} color={theme.colors.foregroundMuted} />,
archive: <Archive size={16} color={theme.colors.foregroundMuted} />,
}),
[theme.colors.foregroundMuted],
);
const { gitActions, isGit } = useGitActions({ serverId, cwd, icons });
@@ -1,18 +1,19 @@
import type { WorkspaceDescriptor } from '@/stores/session-store'
import { projectDisplayNameFromProjectId } from '@/utils/project-display-name'
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
export function resolveWorkspaceHeader(input: {
workspace: WorkspaceDescriptor
}): { title: string; subtitle: string } {
export function resolveWorkspaceHeader(input: { workspace: WorkspaceDescriptor }): {
title: string;
subtitle: string;
} {
return {
title: input.workspace.name,
subtitle: projectDisplayNameFromProjectId(input.workspace.projectId),
}
};
}
export function shouldRenderMissingWorkspaceDescriptor(input: {
workspace: WorkspaceDescriptor | null
hasHydratedWorkspaces: boolean
workspace: WorkspaceDescriptor | null;
hasHydratedWorkspaces: boolean;
}): boolean {
return !input.workspace && input.hasHydratedWorkspaces
return !input.workspace && input.hasHydratedWorkspaces;
}
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { deriveWorkspacePaneState, getWorkspacePaneDescriptors } from "@/screens/workspace/workspace-pane-state";
import {
deriveWorkspacePaneState,
getWorkspacePaneDescriptors,
} from "@/screens/workspace/workspace-pane-state";
import type { WorkspaceLayout } from "@/stores/workspace-layout-store";
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
@@ -182,10 +182,9 @@ export function deriveWorkspacePaneState(input: {
tabs: normalizedTabs.tabs,
focusedTabId,
activeTabId,
activeTab:
activeTabId
? normalizedTabs.tabs.find((tab) => tab.descriptor.tabId === activeTabId) ?? null
: null,
activeTab: activeTabId
? (normalizedTabs.tabs.find((tab) => tab.descriptor.tabId === activeTabId) ?? null)
: null,
};
}
@@ -38,11 +38,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ExplorerSidebar } from "@/components/explorer-sidebar";
import { SplitContainer } from "@/components/split-container";
import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon";
@@ -51,9 +47,7 @@ import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-an
import { useToast } from "@/contexts/toast-context";
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store";
import {
useSessionStore,
} from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store";
import {
buildWorkspaceTabPersistenceKey,
collectAllTabs,
@@ -63,9 +57,7 @@ import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-s
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import {
decodeWorkspaceIdFromPathSegment,
} from "@/utils/host-routes";
import { decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
import {
normalizeWorkspaceTabTarget,
@@ -99,9 +91,7 @@ import {
shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
} from "@/screens/workspace/workspace-agent-visibility";
import {
deriveWorkspacePaneState,
} from "@/screens/workspace/workspace-pane-state";
import { deriveWorkspacePaneState } from "@/screens/workspace/workspace-pane-state";
import {
buildWorkspacePaneContentModel,
WorkspacePaneContent,
@@ -248,8 +238,7 @@ function WorkspaceDocumentTitleEffect({
return;
}
const resolvedLabel = label.trim();
document.title =
titleState === "loading" ? "Loading..." : resolvedLabel || "Workspace";
document.title = titleState === "loading" ? "Loading..." : resolvedLabel || "Workspace";
}, [label, titleState]);
return null;
@@ -326,17 +315,9 @@ function MobileWorkspaceTabOption({
(open || pressed) && styles.mobileTabMenuTriggerActive,
]}
>
<Ellipsis
size={theme.iconSize.sm}
color={theme.colors.foregroundMuted}
/>
<Ellipsis size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
width={220}
testID={menuTestIDBase}
>
<DropdownMenuContent side="bottom" align="end" width={220} testID={menuTestIDBase}>
{menuEntries.map((entry) =>
entry.kind === "separator" ? (
<DropdownMenuSeparator key={entry.key} />
@@ -350,7 +331,7 @@ function MobileWorkspaceTabOption({
>
{entry.label}
</DropdownMenuItem>
)
),
)}
</DropdownMenuContent>
</DropdownMenu>
@@ -389,19 +370,13 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
}, [tabs]);
return (
<View
style={styles.mobileTabsRow}
testID="workspace-tabs-row"
>
<View style={styles.mobileTabsRow} testID="workspace-tabs-row">
<Pressable
ref={anchorRef}
testID="workspace-tab-switcher-trigger"
accessibilityRole="button"
accessibilityLabel={`Switch tabs (${tabs.length} open)`}
style={({ pressed }) => [
styles.switcherTrigger,
pressed && styles.switcherTriggerPressed,
]}
style={({ pressed }) => [styles.switcherTrigger, pressed && styles.switcherTriggerPressed]}
onPress={() => setIsOpen(true)}
>
<View style={styles.switcherTriggerLeft}>
@@ -458,10 +433,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
);
});
export function WorkspaceScreen({
serverId,
workspaceId,
}: WorkspaceScreenProps) {
export function WorkspaceScreen({ serverId, workspaceId }: WorkspaceScreenProps) {
const isFocused = useIsFocused();
if (!isFocused) {
@@ -470,10 +442,7 @@ export function WorkspaceScreen({
return (
<ExplorerSidebarAnimationProvider>
<WorkspaceScreenContent
serverId={serverId}
workspaceId={workspaceId}
/>
<WorkspaceScreenContent serverId={serverId} workspaceId={workspaceId} />
</ExplorerSidebarAnimationProvider>
);
}
@@ -487,38 +456,31 @@ function useCloseTabs(): UseCloseTabsResult {
const pendingRef = useRef(new Set<string>());
const [closingTabIds, setClosingTabIds] = useState<Set<string>>(EMPTY_SET);
const closeTab = useCallback(
async (tabId: string, action: () => Promise<void>) => {
const normalized = tabId.trim();
if (!normalized || pendingRef.current.has(normalized)) {
return;
}
pendingRef.current.add(normalized);
const closeTab = useCallback(async (tabId: string, action: () => Promise<void>) => {
const normalized = tabId.trim();
if (!normalized || pendingRef.current.has(normalized)) {
return;
}
pendingRef.current.add(normalized);
setClosingTabIds(new Set(pendingRef.current));
try {
await action();
} finally {
pendingRef.current.delete(normalized);
setClosingTabIds(new Set(pendingRef.current));
try {
await action();
} finally {
pendingRef.current.delete(normalized);
setClosingTabIds(new Set(pendingRef.current));
}
},
[]
);
}
}, []);
return { closingTabIds, closeTab };
}
function WorkspaceScreenContent({
serverId,
workspaceId,
}: WorkspaceScreenProps) {
function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const isDarkMode = useColorScheme() === "dark";
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
const toast = useToast();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
const normalizedWorkspaceId =
@@ -547,7 +509,7 @@ function WorkspaceScreenContent({
const terminalsQueryKey = useMemo(
() => ["terminals", normalizedServerId, normalizedWorkspaceId] as const,
[normalizedServerId, normalizedWorkspaceId]
[normalizedServerId, normalizedWorkspaceId],
);
type ListTerminalsPayload = ListTerminalsResponse["payload"];
const terminalsQuery = useQuery({
@@ -575,20 +537,17 @@ function WorkspaceScreenContent({
onSuccess: (payload, input) => {
const createdTerminal = payload.terminal;
if (createdTerminal) {
queryClient.setQueryData<ListTerminalsPayload>(
terminalsQueryKey,
(current) => {
const nextTerminals = upsertTerminalListEntry({
terminals: current?.terminals ?? [],
terminal: createdTerminal,
});
return {
cwd: current?.cwd ?? normalizedWorkspaceId,
terminals: nextTerminals,
requestId: current?.requestId ?? `terminal-create-${createdTerminal.id}`,
};
}
);
queryClient.setQueryData<ListTerminalsPayload>(terminalsQueryKey, (current) => {
const nextTerminals = upsertTerminalListEntry({
terminals: current?.terminals ?? [],
terminal: createdTerminal,
});
return {
cwd: current?.cwd ?? normalizedWorkspaceId,
terminals: nextTerminals,
requestId: current?.requestId ?? `terminal-create-${createdTerminal.id}`,
};
});
}
void queryClient.invalidateQueries({ queryKey: terminalsQueryKey });
@@ -671,23 +630,19 @@ function WorkspaceScreenContent({
if (!client) {
throw new Error("Host is not connected");
}
return (await client.getCheckoutStatus(
normalizedWorkspaceId
)) as CheckoutStatusPayload;
return (await client.getCheckoutStatus(normalizedWorkspaceId)) as CheckoutStatusPayload;
},
staleTime: 15_000,
});
const workspaceDescriptor = useSessionStore(
(state) =>
state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ??
null
(state) => state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ?? null,
);
const hasHydratedWorkspaces = useSessionStore(
(state) => state.sessions[normalizedServerId]?.hasHydratedWorkspaces ?? false
(state) => state.sessions[normalizedServerId]?.hasHydratedWorkspaces ?? false,
);
const hasHydratedAgents = useSessionStore(
(state) => state.sessions[normalizedServerId]?.hasHydratedAgents ?? false
(state) => state.sessions[normalizedServerId]?.hasHydratedAgents ?? false,
);
const workspaceHeader = workspaceDescriptor
? resolveWorkspaceHeader({ workspace: workspaceDescriptor })
@@ -700,22 +655,16 @@ function WorkspaceScreenContent({
? trimNonEmpty(checkoutQuery.data.currentBranch)
: null;
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore(
(state) => state.desktop.fileExplorerOpen
);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
const activateExplorerTabForCheckout = usePanelStore(
(state) => state.activateExplorerTabForCheckout
(state) => state.activateExplorerTabForCheckout,
);
const closeToAgent = usePanelStore((state) => state.closeToAgent);
const setActiveExplorerCheckout = usePanelStore(
(state) => state.setActiveExplorerCheckout
);
const setActiveExplorerCheckout = usePanelStore((state) => state.setActiveExplorerCheckout);
const isExplorerOpen = isMobile
? mobileView === "file-explorer"
: desktopFileExplorerOpen;
const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
const activeExplorerCheckout = useMemo<ExplorerCheckoutContext | null>(() => {
if (!normalizedServerId || !normalizedWorkspaceId.startsWith("/")) {
@@ -738,11 +687,7 @@ function WorkspaceScreenContent({
}
activateExplorerTabForCheckout(activeExplorerCheckout);
openFileExplorer();
}, [
activateExplorerTabForCheckout,
activeExplorerCheckout,
openFileExplorer,
]);
}, [activateExplorerTabForCheckout, activeExplorerCheckout, openFileExplorer]);
const handleToggleExplorer = useCallback(() => {
if (isExplorerOpen) {
@@ -779,15 +724,15 @@ function WorkspaceScreenContent({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
}),
[normalizedServerId, normalizedWorkspaceId]
[normalizedServerId, normalizedWorkspaceId],
);
const workspaceLayout = useWorkspaceLayoutStore((state) =>
persistenceKey ? state.layoutByWorkspace[persistenceKey] ?? null : null
persistenceKey ? (state.layoutByWorkspace[persistenceKey] ?? null) : null,
);
const uiTabs = useMemo(
() => (workspaceLayout ? collectAllTabs(workspaceLayout.root) : EMPTY_UI_TABS),
[workspaceLayout]
[workspaceLayout],
);
const openWorkspaceTab = useWorkspaceLayoutStore((state) => state.openTab);
const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab);
@@ -802,8 +747,8 @@ function WorkspaceScreenContent({
const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane);
const pinnedAgentIds = useWorkspaceLayoutStore((state) =>
persistenceKey
? state.pinnedAgentIdsByWorkspace[persistenceKey] ?? EMPTY_PINNED_AGENT_IDS
: EMPTY_PINNED_AGENT_IDS
? (state.pinnedAgentIdsByWorkspace[persistenceKey] ?? EMPTY_PINNED_AGENT_IDS)
: EMPTY_PINNED_AGENT_IDS,
);
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
const { closingTabIds, closeTab } = useCloseTabs();
@@ -822,7 +767,7 @@ function WorkspaceScreenContent({
}
closeWorkspaceTab(persistenceKey, normalizedTabId);
},
[closeWorkspaceTab, persistenceKey, unpinWorkspaceAgent]
[closeWorkspaceTab, persistenceKey, unpinWorkspaceAgent],
);
const focusedPaneTabState = useMemo(
@@ -831,7 +776,7 @@ function WorkspaceScreenContent({
layout: workspaceLayout,
tabs: uiTabs,
}),
[uiTabs, workspaceLayout]
[uiTabs, workspaceLayout],
);
const setFocusedAgentId = useSessionStore((state) => state.setFocusedAgentId);
const focusedPaneAgentId = useMemo(() => {
@@ -876,7 +821,7 @@ function WorkspaceScreenContent({
}
return tabId;
},
[focusWorkspaceTab, focusedPaneTabState, openWorkspaceTab, persistenceKey, uiTabs]
[focusWorkspaceTab, focusedPaneTabState, openWorkspaceTab, persistenceKey, uiTabs],
);
const openWorkspaceDraftTab = useCallback(
@@ -900,7 +845,7 @@ function WorkspaceScreenContent({
}
return tabId;
},
[ensureWorkspaceTab, focusWorkspaceTab, openWorkspaceTab, persistenceKey]
[ensureWorkspaceTab, focusWorkspaceTab, openWorkspaceTab, persistenceKey],
);
useEffect(() => {
@@ -919,10 +864,10 @@ function WorkspaceScreenContent({
for (const agentId of workspaceAgentVisibility.activeAgentIds) {
const representedByTarget = uiTabs.some(
(tab) => tab.target.kind === "agent" && tab.target.agentId === agentId
(tab) => tab.target.kind === "agent" && tab.target.agentId === agentId,
);
const representedByDeterministicTabId = uiTabs.some(
(tab) => tab.tabId === `agent_${agentId}`
(tab) => tab.tabId === `agent_${agentId}`,
);
if (
hasActivePendingDraftCreateInWorkspace &&
@@ -986,7 +931,7 @@ function WorkspaceScreenContent({
const tabs = useMemo<WorkspaceTabDescriptor[]>(
() => focusedPaneTabState.tabs.map((tab) => tab.descriptor),
[focusedPaneTabState.tabs]
[focusedPaneTabState.tabs],
);
const navigateToTabId = useCallback(
@@ -996,7 +941,7 @@ function WorkspaceScreenContent({
}
focusWorkspaceTab(persistenceKey, tabId);
},
[focusWorkspaceTab, persistenceKey]
[focusWorkspaceTab, persistenceKey],
);
const emptyWorkspaceSeedRef = useRef<string | null>(null);
@@ -1041,7 +986,7 @@ function WorkspaceScreenContent({
navigateToTabId(tabId);
}
},
[closeToAgent, isMobile, navigateToTabId, openWorkspaceTab, persistenceKey]
[closeToAgent, isMobile, navigateToTabId, openWorkspaceTab, persistenceKey],
);
const handleOpenFileFromChat = useCallback(
@@ -1052,13 +997,11 @@ function WorkspaceScreenContent({
}
handleOpenFileFromExplorer(normalizedFilePath);
},
[handleOpenFileFromExplorer]
[handleOpenFileFromExplorer],
);
const [hoveredTabKey, setHoveredTabKey] = useState<string | null>(null);
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(
null
);
const [hoveredCloseTabKey, setHoveredCloseTabKey] = useState<string | null>(null);
const tabByKey = useMemo(() => {
const map = new Map<string, WorkspaceTabDescriptor>();
@@ -1090,28 +1033,31 @@ function WorkspaceScreenContent({
label: getFallbackTabOptionLabel(tab),
description: getFallbackTabOptionDescription(tab),
})),
[tabs]
[tabs],
);
const handleCreateDraftTab = useCallback(() => {
openWorkspaceDraftTab();
}, [openWorkspaceDraftTab]);
const handleCreateTerminal = useCallback((input?: { paneId?: string }) => {
if (createTerminalMutation.isPending) {
return;
}
if (!normalizedWorkspaceId.startsWith("/")) {
return;
}
createTerminalMutation.mutate(input);
}, [createTerminalMutation, normalizedWorkspaceId]);
const handleCreateTerminal = useCallback(
(input?: { paneId?: string }) => {
if (createTerminalMutation.isPending) {
return;
}
if (!normalizedWorkspaceId.startsWith("/")) {
return;
}
createTerminalMutation.mutate(input);
},
[createTerminalMutation, normalizedWorkspaceId],
);
const handleSelectSwitcherTab = useCallback(
(key: string) => {
navigateToTabId(key);
},
[navigateToTabId]
[navigateToTabId],
);
const handleSelectNewTabOption = useCallback(
@@ -1125,7 +1071,7 @@ function WorkspaceScreenContent({
handleCreateTerminal({ paneId: selection.paneId });
}
},
[focusWorkspacePane, handleCreateDraftTab, handleCreateTerminal, persistenceKey]
[focusWorkspacePane, handleCreateDraftTab, handleCreateTerminal, persistenceKey],
);
const handleCreateDraftSplit = useCallback(
@@ -1142,7 +1088,7 @@ function WorkspaceScreenContent({
focusWorkspacePane(persistenceKey, paneId);
openWorkspaceDraftTab();
},
[focusWorkspacePane, openWorkspaceDraftTab, persistenceKey, splitWorkspacePaneEmpty]
[focusWorkspacePane, openWorkspaceDraftTab, persistenceKey, splitWorkspacePaneEmpty],
);
const killTerminalAsync = killTerminalMutation.mutateAsync;
@@ -1166,20 +1112,15 @@ function WorkspaceScreenContent({
setHoveredTabKey((current) => (current === tabId ? null : current));
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
queryClient.setQueryData<ListTerminalsPayload>(
terminalsQueryKey,
(current) => {
if (!current) {
return current;
}
return {
...current,
terminals: current.terminals.filter(
(terminal) => terminal.id !== terminalId
),
};
queryClient.setQueryData<ListTerminalsPayload>(terminalsQueryKey, (current) => {
if (!current) {
return current;
}
);
return {
...current,
terminals: current.terminals.filter((terminal) => terminal.id !== terminalId),
};
});
if (persistenceKey) {
closeWorkspaceTabWithCleanup({
@@ -1196,7 +1137,7 @@ function WorkspaceScreenContent({
persistenceKey,
queryClient,
terminalsQueryKey,
]
],
);
const handleCloseAgentTab = useCallback(
@@ -1229,13 +1170,7 @@ function WorkspaceScreenContent({
}
});
},
[
archiveAgent,
closeTab,
closeWorkspaceTabWithCleanup,
normalizedServerId,
persistenceKey,
]
[archiveAgent, closeTab, closeWorkspaceTabWithCleanup, normalizedServerId, persistenceKey],
);
const handleCloseDraftOrFileTab = useCallback(
@@ -1246,7 +1181,7 @@ function WorkspaceScreenContent({
closeWorkspaceTabWithCleanup({ tabId });
}
},
[closeWorkspaceTabWithCleanup, persistenceKey]
[closeWorkspaceTabWithCleanup, persistenceKey],
);
const handleCloseTabById = useCallback(
@@ -1265,19 +1200,31 @@ function WorkspaceScreenContent({
}
handleCloseDraftOrFileTab(tabId);
},
[allTabDescriptorsById, handleCloseAgentTab, handleCloseDraftOrFileTab, handleCloseTerminalTab]
[allTabDescriptorsById, handleCloseAgentTab, handleCloseDraftOrFileTab, handleCloseTerminalTab],
);
const prevCloseTabDeps = useRef({ allTabDescriptorsById, handleCloseAgentTab, handleCloseDraftOrFileTab, handleCloseTerminalTab });
const prevCloseTabDeps = useRef({
allTabDescriptorsById,
handleCloseAgentTab,
handleCloseDraftOrFileTab,
handleCloseTerminalTab,
});
useEffect(() => {
const prev = prevCloseTabDeps.current;
const changed: string[] = [];
if (prev.allTabDescriptorsById !== allTabDescriptorsById) changed.push('allTabDescriptorsById');
if (prev.handleCloseAgentTab !== handleCloseAgentTab) changed.push('handleCloseAgentTab');
if (prev.handleCloseDraftOrFileTab !== handleCloseDraftOrFileTab) changed.push('handleCloseDraftOrFileTab');
if (prev.handleCloseTerminalTab !== handleCloseTerminalTab) changed.push('handleCloseTerminalTab');
if (changed.length > 0) console.log('[handleCloseTabById] deps changed:', changed.join(', '));
prevCloseTabDeps.current = { allTabDescriptorsById, handleCloseAgentTab, handleCloseDraftOrFileTab, handleCloseTerminalTab };
if (prev.allTabDescriptorsById !== allTabDescriptorsById) changed.push("allTabDescriptorsById");
if (prev.handleCloseAgentTab !== handleCloseAgentTab) changed.push("handleCloseAgentTab");
if (prev.handleCloseDraftOrFileTab !== handleCloseDraftOrFileTab)
changed.push("handleCloseDraftOrFileTab");
if (prev.handleCloseTerminalTab !== handleCloseTerminalTab)
changed.push("handleCloseTerminalTab");
if (changed.length > 0) console.log("[handleCloseTabById] deps changed:", changed.join(", "));
prevCloseTabDeps.current = {
allTabDescriptorsById,
handleCloseAgentTab,
handleCloseDraftOrFileTab,
handleCloseTerminalTab,
};
});
const handleCopyAgentId = useCallback(
async (agentId: string) => {
@@ -1289,13 +1236,14 @@ function WorkspaceScreenContent({
toast.error("Copy failed");
}
},
[toast]
[toast],
);
const handleCopyResumeCommand = useCallback(
async (agentId: string) => {
if (!agentId) return;
const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null;
const agent =
useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null;
const providerSessionId =
agent?.runtimeInfo?.sessionId ?? agent?.persistence?.sessionId ?? null;
if (!agent || !providerSessionId) {
@@ -1320,7 +1268,7 @@ function WorkspaceScreenContent({
toast.error("Copy failed");
}
},
[normalizedServerId, toast]
[normalizedServerId, toast],
);
const handleCopyWorkspacePath = useCallback(async () => {
@@ -1390,7 +1338,10 @@ function WorkspaceScreenContent({
});
}
} catch (error) {
console.warn(`[WorkspaceScreen] Failed to close terminal tab ${logLabel}`, { terminalId, error });
console.warn(`[WorkspaceScreen] Failed to close terminal tab ${logLabel}`, {
terminalId,
error,
});
}
});
}
@@ -1409,7 +1360,10 @@ function WorkspaceScreenContent({
});
}
} catch (error) {
console.warn(`[WorkspaceScreen] Failed to archive agent tab ${logLabel}`, { agentId, error });
console.warn(`[WorkspaceScreen] Failed to archive agent tab ${logLabel}`, {
agentId,
error,
});
}
});
}
@@ -1435,7 +1389,7 @@ function WorkspaceScreenContent({
persistenceKey,
queryClient,
terminalsQueryKey,
]
],
);
const handleCloseTabsToLeftInPane = useCallback(
@@ -1450,14 +1404,14 @@ function WorkspaceScreenContent({
logLabel: "to the left",
});
},
[handleBulkCloseTabs]
[handleBulkCloseTabs],
);
const handleCloseTabsToLeft = useCallback(
async (tabId: string) => {
await handleCloseTabsToLeftInPane(tabId, tabs);
},
[handleCloseTabsToLeftInPane, tabs]
[handleCloseTabsToLeftInPane, tabs],
);
const handleCloseTabsToRightInPane = useCallback(
@@ -1472,14 +1426,14 @@ function WorkspaceScreenContent({
logLabel: "to the right",
});
},
[handleBulkCloseTabs]
[handleBulkCloseTabs],
);
const handleCloseTabsToRight = useCallback(
async (tabId: string) => {
await handleCloseTabsToRightInPane(tabId, tabs);
},
[handleCloseTabsToRightInPane, tabs]
[handleCloseTabsToRightInPane, tabs],
);
const handleCloseOtherTabsInPane = useCallback(
@@ -1491,14 +1445,14 @@ function WorkspaceScreenContent({
logLabel: "from close other tabs",
});
},
[handleBulkCloseTabs]
[handleBulkCloseTabs],
);
const handleCloseOtherTabs = useCallback(
async (tabId: string) => {
await handleCloseOtherTabsInPane(tabId, tabs);
},
[handleCloseOtherTabsInPane, tabs]
[handleCloseOtherTabsInPane, tabs],
);
const handleWorkspaceTabAction = useCallback(
@@ -1535,7 +1489,7 @@ function WorkspaceScreenContent({
return false;
}
},
[activeTabId, handleCloseTabById, handleCreateDraftTab, navigateToTabId, tabs]
[activeTabId, handleCloseTabById, handleCreateDraftTab, navigateToTabId, tabs],
);
const handleWorkspacePaneAction = useCallback(
@@ -1630,7 +1584,7 @@ function WorkspaceScreenContent({
focusedPaneTabState.activeTabId,
focusedPaneTabState.pane,
workspaceLayout,
]
],
);
useKeyboardActionHandler({
@@ -1725,23 +1679,45 @@ function WorkspaceScreenContent({
openWorkspaceTab,
persistenceKey,
retargetWorkspaceTab,
]
],
);
const prevBuildDeps = useRef({ handleCloseTabById, handleOpenFileFromChat, focusWorkspacePane, navigateToTabId, normalizedServerId, normalizedWorkspaceId, openWorkspaceTab, persistenceKey, retargetWorkspaceTab });
const prevBuildDeps = useRef({
handleCloseTabById,
handleOpenFileFromChat,
focusWorkspacePane,
navigateToTabId,
normalizedServerId,
normalizedWorkspaceId,
openWorkspaceTab,
persistenceKey,
retargetWorkspaceTab,
});
useEffect(() => {
const prev = prevBuildDeps.current;
const changed: string[] = [];
if (prev.handleCloseTabById !== handleCloseTabById) changed.push('handleCloseTabById');
if (prev.handleOpenFileFromChat !== handleOpenFileFromChat) changed.push('handleOpenFileFromChat');
if (prev.focusWorkspacePane !== focusWorkspacePane) changed.push('focusWorkspacePane');
if (prev.navigateToTabId !== navigateToTabId) changed.push('navigateToTabId');
if (prev.normalizedServerId !== normalizedServerId) changed.push('normalizedServerId');
if (prev.normalizedWorkspaceId !== normalizedWorkspaceId) changed.push('normalizedWorkspaceId');
if (prev.openWorkspaceTab !== openWorkspaceTab) changed.push('openWorkspaceTab');
if (prev.persistenceKey !== persistenceKey) changed.push('persistenceKey');
if (prev.retargetWorkspaceTab !== retargetWorkspaceTab) changed.push('retargetWorkspaceTab');
if (changed.length > 0) console.log('[buildPaneContentModel] deps changed:', changed.join(', '));
prevBuildDeps.current = { handleCloseTabById, handleOpenFileFromChat, focusWorkspacePane, navigateToTabId, normalizedServerId, normalizedWorkspaceId, openWorkspaceTab, persistenceKey, retargetWorkspaceTab };
if (prev.handleCloseTabById !== handleCloseTabById) changed.push("handleCloseTabById");
if (prev.handleOpenFileFromChat !== handleOpenFileFromChat)
changed.push("handleOpenFileFromChat");
if (prev.focusWorkspacePane !== focusWorkspacePane) changed.push("focusWorkspacePane");
if (prev.navigateToTabId !== navigateToTabId) changed.push("navigateToTabId");
if (prev.normalizedServerId !== normalizedServerId) changed.push("normalizedServerId");
if (prev.normalizedWorkspaceId !== normalizedWorkspaceId) changed.push("normalizedWorkspaceId");
if (prev.openWorkspaceTab !== openWorkspaceTab) changed.push("openWorkspaceTab");
if (prev.persistenceKey !== persistenceKey) changed.push("persistenceKey");
if (prev.retargetWorkspaceTab !== retargetWorkspaceTab) changed.push("retargetWorkspaceTab");
if (changed.length > 0)
console.log("[buildPaneContentModel] deps changed:", changed.join(", "));
prevBuildDeps.current = {
handleCloseTabById,
handleOpenFileFromChat,
focusWorkspacePane,
navigateToTabId,
normalizedServerId,
normalizedWorkspaceId,
openWorkspaceTab,
persistenceKey,
retargetWorkspaceTab,
};
});
const activePaneContent = useMemo(
() =>
@@ -1753,15 +1729,26 @@ function WorkspaceScreenContent({
focusPaneBeforeOpen: false,
})
: null,
[activeTabDescriptor, buildPaneContentModel, focusedPaneTabState.pane?.id]
[activeTabDescriptor, buildPaneContentModel, focusedPaneTabState.pane?.id],
);
const prevActivePaneDeps = useRef({ activeTabDescriptor, buildPaneContentModel, paneId: focusedPaneTabState.pane?.id });
const prevActivePaneDeps = useRef({
activeTabDescriptor,
buildPaneContentModel,
paneId: focusedPaneTabState.pane?.id,
});
useEffect(() => {
const prev = prevActivePaneDeps.current;
if (prev.activeTabDescriptor !== activeTabDescriptor) console.log('[activePaneContent] activeTabDescriptor changed');
if (prev.buildPaneContentModel !== buildPaneContentModel) console.log('[activePaneContent] buildPaneContentModel changed');
if (prev.paneId !== focusedPaneTabState.pane?.id) console.log('[activePaneContent] paneId changed');
prevActivePaneDeps.current = { activeTabDescriptor, buildPaneContentModel, paneId: focusedPaneTabState.pane?.id };
if (prev.activeTabDescriptor !== activeTabDescriptor)
console.log("[activePaneContent] activeTabDescriptor changed");
if (prev.buildPaneContentModel !== buildPaneContentModel)
console.log("[activePaneContent] buildPaneContentModel changed");
if (prev.paneId !== focusedPaneTabState.pane?.id)
console.log("[activePaneContent] paneId changed");
prevActivePaneDeps.current = {
activeTabDescriptor,
buildPaneContentModel,
paneId: focusedPaneTabState.pane?.id,
};
});
const content = shouldRenderMissingWorkspaceDescriptor({
workspace: workspaceDescriptor,
@@ -1844,36 +1831,26 @@ function WorkspaceScreenContent({
return (
<Icon
size={theme.iconSize.md}
color={hovered || open ? theme.colors.foreground : theme.colors.foregroundMuted}
color={
hovered || open
? theme.colors.foreground
: theme.colors.foregroundMuted
}
/>
);
}}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
width={220}
testID="workspace-header-menu"
>
<DropdownMenuContent align="start" width={220} testID="workspace-header-menu">
<DropdownMenuItem
testID="workspace-header-new-agent"
leading={
<SquarePen
size={16}
color={theme.colors.foregroundMuted}
/>
}
leading={<SquarePen size={16} color={theme.colors.foregroundMuted} />}
onSelect={handleCreateDraftTab}
>
New agent
</DropdownMenuItem>
<DropdownMenuItem
testID="workspace-header-new-terminal"
leading={
<SquareTerminal
size={16}
color={theme.colors.foregroundMuted}
/>
}
leading={<SquareTerminal size={16} color={theme.colors.foregroundMuted} />}
disabled={createTerminalMutation.isPending}
onSelect={handleCreateTerminal}
>
@@ -1881,12 +1858,7 @@ function WorkspaceScreenContent({
</DropdownMenuItem>
<DropdownMenuItem
testID="workspace-header-copy-path"
leading={
<Copy
size={16}
color={theme.colors.foregroundMuted}
/>
}
leading={<Copy size={16} color={theme.colors.foregroundMuted} />}
disabled={!normalizedWorkspaceId.startsWith("/")}
onSelect={handleCopyWorkspacePath}
>
@@ -1895,12 +1867,7 @@ function WorkspaceScreenContent({
{currentBranchName ? (
<DropdownMenuItem
testID="workspace-header-copy-branch-name"
leading={
<Copy
size={16}
color={theme.colors.foregroundMuted}
/>
}
leading={<Copy size={16} color={theme.colors.foregroundMuted} />}
onSelect={handleCopyBranchName}
>
Copy branch name
@@ -1933,14 +1900,20 @@ function WorkspaceScreenContent({
>
{({ hovered, pressed }) => {
const active = isExplorerOpen || hovered || pressed;
const iconColor = active ? theme.colors.foreground : theme.colors.foregroundMuted;
const iconColor = active
? theme.colors.foreground
: theme.colors.foregroundMuted;
return (
<>
<SourceControlPanelIcon size={theme.iconSize.md} color={iconColor} />
{workspaceDescriptor?.diffStat ? (
<View style={styles.diffStatRow}>
<Text style={styles.diffStatAdditions}>+{workspaceDescriptor.diffStat.additions}</Text>
<Text style={styles.diffStatDeletions}>-{workspaceDescriptor.diffStat.deletions}</Text>
<Text style={styles.diffStatAdditions}>
+{workspaceDescriptor.diffStat.additions}
</Text>
<Text style={styles.diffStatDeletions}>
-{workspaceDescriptor.diffStat.deletions}
</Text>
</View>
) : null}
</>
@@ -1963,7 +1936,10 @@ function WorkspaceScreenContent({
accessibilityState={{ expanded: isExplorerOpen }}
>
{({ hovered }) => {
const color = isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
const color =
isExplorerOpen || hovered
? theme.colors.foreground
: theme.colors.foregroundMuted;
return <PanelRight size={theme.iconSize.md} color={color} />;
}}
</HeaderToggleButton>
@@ -1982,10 +1958,19 @@ function WorkspaceScreenContent({
accessibilityState={{ expanded: isExplorerOpen }}
>
{({ hovered }) => {
const color = isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
return isGitCheckout
? <SourceControlPanelIcon size={theme.iconSize.lg} color={color} strokeWidth={1.5} />
: <PanelRight size={theme.iconSize.lg} color={color} />;
const color =
isExplorerOpen || hovered
? theme.colors.foreground
: theme.colors.foregroundMuted;
return isGitCheckout ? (
<SourceControlPanelIcon
size={theme.iconSize.lg}
color={color}
strokeWidth={1.5}
/>
) : (
<PanelRight size={theme.iconSize.lg} color={color} />
);
}}
</HeaderToggleButton>
) : null}
@@ -1,53 +1,53 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it } from "vitest";
import {
resolveWorkspaceHeader,
shouldRenderMissingWorkspaceDescriptor,
} from './workspace-header-source'
import { buildSidebarProjectsFromWorkspaces } from '@/hooks/use-sidebar-workspaces-list'
import type { WorkspaceDescriptor } from '@/stores/session-store'
} from "./workspace-header-source";
import { buildSidebarProjectsFromWorkspaces } from "@/hooks/use-sidebar-workspaces-list";
import type { WorkspaceDescriptor } from "@/stores/session-store";
describe('workspace source of truth consumption', () => {
it('uses the same descriptor name in header and sidebar row', () => {
describe("workspace source of truth consumption", () => {
it("uses the same descriptor name in header and sidebar row", () => {
const workspace: WorkspaceDescriptor = {
id: '/repo/main',
projectId: 'remote:github.com/getpaseo/paseo',
projectDisplayName: 'getpaseo/paseo',
projectRootPath: '/repo/main',
projectKind: 'git',
workspaceKind: 'local_checkout',
name: 'feat/workspace-sot',
status: 'running',
activityAt: new Date('2026-03-01T00:00:00.000Z'),
id: "/repo/main",
projectId: "remote:github.com/getpaseo/paseo",
projectDisplayName: "getpaseo/paseo",
projectRootPath: "/repo/main",
projectKind: "git",
workspaceKind: "local_checkout",
name: "feat/workspace-sot",
status: "running",
activityAt: new Date("2026-03-01T00:00:00.000Z"),
diffStat: null,
}
};
const header = resolveWorkspaceHeader({ workspace })
const header = resolveWorkspaceHeader({ workspace });
const sidebarProjects = buildSidebarProjectsFromWorkspaces({
serverId: 'srv',
serverId: "srv",
workspaces: [workspace],
projectOrder: [],
workspaceOrderByScope: {},
})
});
expect(header.title).toBe('feat/workspace-sot')
expect(header.subtitle).toBe('getpaseo/paseo')
expect(sidebarProjects[0]?.workspaces[0]?.name).toBe(header.title)
expect(sidebarProjects[0]?.workspaces[0]?.statusBucket).toBe('running')
})
expect(header.title).toBe("feat/workspace-sot");
expect(header.subtitle).toBe("getpaseo/paseo");
expect(sidebarProjects[0]?.workspaces[0]?.name).toBe(header.title);
expect(sidebarProjects[0]?.workspaces[0]?.statusBucket).toBe("running");
});
it('renders explicit missing state only after workspace hydration', () => {
it("renders explicit missing state only after workspace hydration", () => {
expect(
shouldRenderMissingWorkspaceDescriptor({
workspace: null,
hasHydratedWorkspaces: true,
})
).toBe(true)
}),
).toBe(true);
expect(
shouldRenderMissingWorkspaceDescriptor({
workspace: null,
hasHydratedWorkspaces: false,
})
).toBe(false)
})
})
}),
).toBe(false);
});
});
@@ -39,7 +39,7 @@ function clamp(value: number, min: number, max: number): number {
}
export function computeWorkspaceTabLayout(
input: WorkspaceTabLayoutInput
input: WorkspaceTabLayoutInput,
): WorkspaceTabLayoutResult {
const tabCount = input.tabLabelLengths.length;
if (tabCount === 0) {
@@ -52,23 +52,25 @@ export function computeWorkspaceTabLayout(
const availableWidth = Math.max(
0,
input.viewportWidth - input.metrics.rowHorizontalInset * 2 - input.metrics.actionsReservedWidth
input.viewportWidth - input.metrics.rowHorizontalInset * 2 - input.metrics.actionsReservedWidth,
);
const rowOverhead =
input.metrics.rowPaddingHorizontal * 2 + Math.max(tabCount - 1, 0) * input.metrics.tabGap;
const availableTabsWidth = Math.max(0, availableWidth - rowOverhead);
const iconOnlyTabWidth =
input.metrics.tabIconWidth + input.metrics.tabHorizontalPadding * 2 + input.metrics.closeButtonWidth;
input.metrics.tabIconWidth +
input.metrics.tabHorizontalPadding * 2 +
input.metrics.closeButtonWidth;
const iconOnlyTotalTabsWidth = iconOnlyTabWidth * tabCount;
const requiresHorizontalScrollFallback = availableTabsWidth < iconOnlyTotalTabsWidth;
const resolvedWidths = new Array(tabCount).fill(
requiresHorizontalScrollFallback
? iconOnlyTabWidth
: clamp(availableTabsWidth / tabCount, iconOnlyTabWidth, input.metrics.maxTabWidth)
: clamp(availableTabsWidth / tabCount, iconOnlyTabWidth, input.metrics.maxTabWidth),
);
const roundedWidths = resolvedWidths.map((width) =>
Math.round(clamp(width, iconOnlyTabWidth, input.metrics.maxTabWidth))
Math.round(clamp(width, iconOnlyTabWidth, input.metrics.maxTabWidth)),
);
return {
@@ -34,9 +34,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
onCloseOtherTabs,
});
expect(
entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)
).toEqual([
expect(entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)).toEqual([
"Copy resume command",
"Copy agent id",
"Close to the left",
@@ -61,9 +59,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
onCloseOtherTabs: vi.fn(),
});
expect(
entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)
).toEqual([
expect(entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)).toEqual([
"Copy resume command",
"Copy agent id",
"Close tabs above",
@@ -94,7 +90,7 @@ describe("buildWorkspaceTabMenuEntries", () => {
});
expect(entries.some((entry) => entry.kind === "item" && entry.label === "Copy agent id")).toBe(
false
false,
);
expect(entries.some((entry) => entry.kind === "separator")).toBe(false);
});
@@ -80,7 +80,7 @@ function getCloseButtonTestId(tab: WorkspaceTabDescriptor): string {
}
export function buildWorkspaceTabMenuEntries(
input: BuildWorkspaceTabMenuEntriesInput
input: BuildWorkspaceTabMenuEntriesInput,
): WorkspaceTabMenuEntry[] {
const {
surface,
@@ -170,7 +170,7 @@ export function buildWorkspaceTabMenuEntries(
}
export function buildWorkspaceDesktopTabActions(
input: BuildWorkspaceDesktopTabActionsInput
input: BuildWorkspaceDesktopTabActionsInput,
): WorkspaceDesktopTabActions {
const contextMenuTestId = `workspace-tab-context-${input.tab.key}`;
return {
@@ -42,7 +42,11 @@ describe("deriveWorkspaceTabModel", () => {
it("applies stored order and appends unordered tabs deterministically", () => {
const model = deriveWorkspaceTabModel({
tabs: [
{ tabId: "terminal_term-1", target: { kind: "terminal", terminalId: "term-1" }, createdAt: 3 },
{
tabId: "terminal_term-1",
target: { kind: "terminal", terminalId: "term-1" },
createdAt: 3,
},
{ tabId: "agent_agent-b", target: { kind: "agent", agentId: "agent-b" }, createdAt: 2 },
{ tabId: "agent_agent-a", target: { kind: "agent", agentId: "agent-a" }, createdAt: 1 },
],
@@ -67,7 +71,7 @@ describe("deriveWorkspaceTabModel", () => {
deriveWorkspaceTabModel({
...base,
focusedTabId: "agent_agent-b",
}).activeTabId
}).activeTabId,
).toBe("agent_agent-b");
expect(deriveWorkspaceTabModel(base).activeTabId).toBe("agent_agent-a");
@@ -3,9 +3,7 @@ import {
deriveWorkspacePaneState,
type WorkspaceDerivedTab,
} from "@/screens/workspace/workspace-pane-state";
import {
buildDeterministicWorkspaceTabId,
} from "@/utils/workspace-tab-identity";
import { buildDeterministicWorkspaceTabId } from "@/utils/workspace-tab-identity";
export interface WorkspaceTabModel {
tabs: WorkspaceDerivedTab[];
@@ -85,7 +85,7 @@ function WorkspaceTabPresentationResolverInner({
descriptor.titleState,
tab.key,
tab.kind,
]
],
);
return <>{children(presentation)}</>;