Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -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)}</>;
|
||||
|
||||
Reference in New Issue
Block a user