Polish workspace header and unify workspace tab creation
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Platform,
|
Platform,
|
||||||
Pressable,
|
Pressable,
|
||||||
@@ -10,13 +9,14 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { ChevronDown, Plus, Terminal } from "lucide-react-native";
|
import { Bot, ChevronDown, Plus, Terminal } from "lucide-react-native";
|
||||||
import { GestureDetector } from "react-native-gesture-handler";
|
import { GestureDetector } from "react-native-gesture-handler";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
||||||
import { ScreenHeader } from "@/components/headers/screen-header";
|
import { ScreenHeader } from "@/components/headers/screen-header";
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import { AgentStatusDot } from "@/components/agent-status-dot";
|
import { ClaudeIcon } from "@/components/icons/claude-icon";
|
||||||
|
import { CodexIcon } from "@/components/icons/codex-icon";
|
||||||
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
||||||
import { TerminalPane } from "@/components/terminal-pane";
|
import { TerminalPane } from "@/components/terminal-pane";
|
||||||
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
|
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
@@ -38,13 +38,15 @@ import {
|
|||||||
checkoutStatusQueryKey,
|
checkoutStatusQueryKey,
|
||||||
type CheckoutStatusPayload,
|
type CheckoutStatusPayload,
|
||||||
} from "@/hooks/use-checkout-status-query";
|
} from "@/hooks/use-checkout-status-query";
|
||||||
import { deriveBranchLabel } from "@/utils/agent-display-info";
|
|
||||||
import { AgentReadyScreen } from "@/screens/agent/agent-ready-screen";
|
import { AgentReadyScreen } from "@/screens/agent/agent-ready-screen";
|
||||||
import type { ListTerminalsResponse } from "@server/shared/messages";
|
import type { ListTerminalsResponse } from "@server/shared/messages";
|
||||||
import { upsertTerminalListEntry } from "@/utils/terminal-list";
|
import { upsertTerminalListEntry } from "@/utils/terminal-list";
|
||||||
|
|
||||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||||
const CREATE_TERMINAL_SWITCHER_ID = "__create_terminal__";
|
const NEW_TAB_AGENT_OPTION_ID = "__new_tab_agent__";
|
||||||
|
const NEW_TAB_TERMINAL_OPTION_ID = "__new_tab_terminal__";
|
||||||
|
|
||||||
|
type TabAvailability = "available" | "invalid" | "unknown";
|
||||||
|
|
||||||
type RouteTabTarget = WorkspaceTabTarget | null;
|
type RouteTabTarget = WorkspaceTabTarget | null;
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ type WorkspaceTabDescriptor =
|
|||||||
key: string;
|
key: string;
|
||||||
kind: "agent";
|
kind: "agent";
|
||||||
agentId: string;
|
agentId: string;
|
||||||
|
provider: Agent["provider"];
|
||||||
label: string;
|
label: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
}
|
}
|
||||||
@@ -93,6 +96,35 @@ function deriveWorkspaceName(workspaceId: string): string {
|
|||||||
return last ?? workspaceId;
|
return last ?? workspaceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deriveWorkspaceHeaderTitle(input: {
|
||||||
|
workspaceName: string;
|
||||||
|
checkout: CheckoutStatusPayload | null;
|
||||||
|
}): string {
|
||||||
|
if (!input.checkout?.isGit) {
|
||||||
|
return input.workspaceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branch = trimNonEmpty(input.checkout.currentBranch ?? null);
|
||||||
|
if (!branch || branch === "HEAD") {
|
||||||
|
return input.workspaceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProviderLabel(provider: Agent["provider"]): string {
|
||||||
|
if (provider === "claude") {
|
||||||
|
return "Claude";
|
||||||
|
}
|
||||||
|
if (provider === "codex") {
|
||||||
|
return "Codex";
|
||||||
|
}
|
||||||
|
if (!provider) {
|
||||||
|
return "Agent";
|
||||||
|
}
|
||||||
|
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeWorkspaceTab(
|
function normalizeWorkspaceTab(
|
||||||
value: WorkspaceTabTarget | null | undefined
|
value: WorkspaceTabTarget | null | undefined
|
||||||
): WorkspaceTabTarget | null {
|
): WorkspaceTabTarget | null {
|
||||||
@@ -151,15 +183,23 @@ function buildTabRoute(input: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTabAvailable(input: {
|
function resolveTabAvailability(input: {
|
||||||
tab: WorkspaceTabTarget;
|
tab: WorkspaceTabTarget;
|
||||||
|
agentsHydrated: boolean;
|
||||||
|
terminalsHydrated: boolean;
|
||||||
agentsById: Map<string, Agent>;
|
agentsById: Map<string, Agent>;
|
||||||
terminalIds: Set<string>;
|
terminalIds: Set<string>;
|
||||||
}): boolean {
|
}): TabAvailability {
|
||||||
if (input.tab.kind === "agent") {
|
if (input.tab.kind === "agent") {
|
||||||
return input.agentsById.has(input.tab.agentId);
|
if (!input.agentsHydrated) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
return input.agentsById.has(input.tab.agentId) ? "available" : "invalid";
|
||||||
}
|
}
|
||||||
return input.terminalIds.has(input.tab.terminalId);
|
if (!input.terminalsHydrated) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
return input.terminalIds.has(input.tab.terminalId) ? "available" : "invalid";
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortAgentsByCreatedAtDescending(agents: Agent[]): Agent[] {
|
function sortAgentsByCreatedAtDescending(agents: Agent[]): Agent[] {
|
||||||
@@ -330,13 +370,20 @@ function WorkspaceScreenContent({
|
|||||||
() => deriveWorkspaceName(normalizedWorkspaceId),
|
() => deriveWorkspaceName(normalizedWorkspaceId),
|
||||||
[normalizedWorkspaceId]
|
[normalizedWorkspaceId]
|
||||||
);
|
);
|
||||||
const branchLabel = useMemo(
|
const headerTitle = useMemo(
|
||||||
() => deriveBranchLabel(checkoutQuery.data ?? null),
|
() =>
|
||||||
[checkoutQuery.data]
|
deriveWorkspaceHeaderTitle({
|
||||||
|
workspaceName,
|
||||||
|
checkout: checkoutQuery.data ?? null,
|
||||||
|
}),
|
||||||
|
[checkoutQuery.data, workspaceName]
|
||||||
);
|
);
|
||||||
const headerTitle = branchLabel ?? workspaceName;
|
|
||||||
|
|
||||||
const isGitCheckout = checkoutQuery.data?.isGit ?? false;
|
const isGitCheckout = checkoutQuery.data?.isGit ?? false;
|
||||||
|
const areWorkspaceAgentsHydrated = useSessionStore(
|
||||||
|
(state) => state.sessions[normalizedServerId]?.hasHydratedAgents ?? false
|
||||||
|
);
|
||||||
|
const areWorkspaceTerminalsHydrated = terminalsQuery.isSuccess;
|
||||||
|
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopFileExplorerOpen = usePanelStore(
|
const desktopFileExplorerOpen = usePanelStore(
|
||||||
@@ -411,8 +458,9 @@ function WorkspaceScreenContent({
|
|||||||
key: `agent:${agent.id}`,
|
key: `agent:${agent.id}`,
|
||||||
kind: "agent",
|
kind: "agent",
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
|
provider: agent.provider,
|
||||||
label: agent.title?.trim() || "New agent",
|
label: agent.title?.trim() || "New agent",
|
||||||
subtitle: "Agent",
|
subtitle: `${formatProviderLabel(agent.provider)} agent`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,31 +523,75 @@ function WorkspaceScreenContent({
|
|||||||
return { kind: "terminal", terminalId: first.terminalId };
|
return { kind: "terminal", terminalId: first.terminalId };
|
||||||
}, [tabs]);
|
}, [tabs]);
|
||||||
|
|
||||||
|
const requestedTabAvailability = useMemo<TabAvailability | null>(() => {
|
||||||
|
if (!requestedTab) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resolveTabAvailability({
|
||||||
|
tab: requestedTab,
|
||||||
|
agentsHydrated: areWorkspaceAgentsHydrated,
|
||||||
|
terminalsHydrated: areWorkspaceTerminalsHydrated,
|
||||||
|
agentsById,
|
||||||
|
terminalIds,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
agentsById,
|
||||||
|
areWorkspaceAgentsHydrated,
|
||||||
|
areWorkspaceTerminalsHydrated,
|
||||||
|
requestedTab,
|
||||||
|
terminalIds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const storedTabAvailability = useMemo<TabAvailability | null>(() => {
|
||||||
|
if (!storedTab) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resolveTabAvailability({
|
||||||
|
tab: storedTab,
|
||||||
|
agentsHydrated: areWorkspaceAgentsHydrated,
|
||||||
|
terminalsHydrated: areWorkspaceTerminalsHydrated,
|
||||||
|
agentsById,
|
||||||
|
terminalIds,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
agentsById,
|
||||||
|
areWorkspaceAgentsHydrated,
|
||||||
|
areWorkspaceTerminalsHydrated,
|
||||||
|
storedTab,
|
||||||
|
terminalIds,
|
||||||
|
]);
|
||||||
|
|
||||||
const resolvedTab = useMemo<WorkspaceTabTarget | null>(() => {
|
const resolvedTab = useMemo<WorkspaceTabTarget | null>(() => {
|
||||||
if (
|
if (requestedTab && requestedTabAvailability !== "invalid") {
|
||||||
requestedTab &&
|
|
||||||
isTabAvailable({
|
|
||||||
tab: requestedTab,
|
|
||||||
agentsById,
|
|
||||||
terminalIds,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return requestedTab;
|
return requestedTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (storedTab && storedTabAvailability !== "invalid") {
|
||||||
storedTab &&
|
|
||||||
isTabAvailable({
|
|
||||||
tab: storedTab,
|
|
||||||
agentsById,
|
|
||||||
terminalIds,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return storedTab;
|
return storedTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallbackTab;
|
return fallbackTab;
|
||||||
}, [agentsById, fallbackTab, requestedTab, storedTab, terminalIds]);
|
}, [fallbackTab, requestedTab, requestedTabAvailability, storedTab, storedTabAvailability]);
|
||||||
|
|
||||||
|
const resolvedTabAvailability = useMemo<TabAvailability | null>(() => {
|
||||||
|
if (!resolvedTab) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveTabAvailability({
|
||||||
|
tab: resolvedTab,
|
||||||
|
agentsHydrated: areWorkspaceAgentsHydrated,
|
||||||
|
terminalsHydrated: areWorkspaceTerminalsHydrated,
|
||||||
|
agentsById,
|
||||||
|
terminalIds,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
agentsById,
|
||||||
|
areWorkspaceAgentsHydrated,
|
||||||
|
areWorkspaceTerminalsHydrated,
|
||||||
|
resolvedTab,
|
||||||
|
terminalIds,
|
||||||
|
]);
|
||||||
|
|
||||||
const navigateToTab = useCallback(
|
const navigateToTab = useCallback(
|
||||||
(tab: WorkspaceTabTarget) => {
|
(tab: WorkspaceTabTarget) => {
|
||||||
@@ -531,6 +623,9 @@ function WorkspaceScreenContent({
|
|||||||
if (!resolvedTab) {
|
if (!resolvedTab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (resolvedTabAvailability !== "available") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLastFocusedTab({
|
setLastFocusedTab({
|
||||||
serverId: normalizedServerId,
|
serverId: normalizedServerId,
|
||||||
@@ -541,6 +636,7 @@ function WorkspaceScreenContent({
|
|||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
resolvedTab,
|
resolvedTab,
|
||||||
|
resolvedTabAvailability,
|
||||||
setLastFocusedTab,
|
setLastFocusedTab,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -569,7 +665,9 @@ function WorkspaceScreenContent({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const [isTabSwitcherOpen, setIsTabSwitcherOpen] = useState(false);
|
const [isTabSwitcherOpen, setIsTabSwitcherOpen] = useState(false);
|
||||||
|
const [isNewTabMenuOpen, setIsNewTabMenuOpen] = useState(false);
|
||||||
const tabSwitcherAnchorRef = useRef<View>(null);
|
const tabSwitcherAnchorRef = useRef<View>(null);
|
||||||
|
const newTabAnchorRef = useRef<View>(null);
|
||||||
|
|
||||||
const tabByKey = useMemo(() => {
|
const tabByKey = useMemo(() => {
|
||||||
const map = new Map<string, WorkspaceTabTarget>();
|
const map = new Map<string, WorkspaceTabTarget>();
|
||||||
@@ -595,21 +693,30 @@ function WorkspaceScreenContent({
|
|||||||
|
|
||||||
const tabSwitcherOptions = useMemo(
|
const tabSwitcherOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
tabs.map((tab) => ({
|
||||||
...tabs.map((tab) => ({
|
id: tab.key,
|
||||||
id: tab.key,
|
label: tab.label,
|
||||||
label: tab.label,
|
description: tab.subtitle,
|
||||||
description: tab.subtitle,
|
})),
|
||||||
})),
|
|
||||||
{
|
|
||||||
id: CREATE_TERMINAL_SWITCHER_ID,
|
|
||||||
label: "New terminal",
|
|
||||||
description: "Create",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[tabs]
|
[tabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const newTabOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: NEW_TAB_AGENT_OPTION_ID,
|
||||||
|
label: "Agent tab",
|
||||||
|
description: "Open the draft agent flow for this workspace",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: NEW_TAB_TERMINAL_OPTION_ID,
|
||||||
|
label: "Terminal tab",
|
||||||
|
description: "Create a new terminal in this workspace",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const activeTabLabel = useMemo(() => {
|
const activeTabLabel = useMemo(() => {
|
||||||
const active = tabs.find((tab) => tab.key === activeTabKey);
|
const active = tabs.find((tab) => tab.key === activeTabKey);
|
||||||
return active?.label ?? "Select tab";
|
return active?.label ?? "Select tab";
|
||||||
@@ -625,6 +732,9 @@ function WorkspaceScreenContent({
|
|||||||
}, [normalizedServerId, normalizedWorkspaceId, router]);
|
}, [normalizedServerId, normalizedWorkspaceId, router]);
|
||||||
|
|
||||||
const handleCreateTerminal = useCallback(() => {
|
const handleCreateTerminal = useCallback(() => {
|
||||||
|
if (createTerminalMutation.isPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!normalizedWorkspaceId.startsWith("/")) {
|
if (!normalizedWorkspaceId.startsWith("/")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -633,11 +743,6 @@ function WorkspaceScreenContent({
|
|||||||
|
|
||||||
const handleSelectSwitcherTab = useCallback(
|
const handleSelectSwitcherTab = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
if (key === CREATE_TERMINAL_SWITCHER_ID) {
|
|
||||||
setIsTabSwitcherOpen(false);
|
|
||||||
handleCreateTerminal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tab = tabByKey.get(key);
|
const tab = tabByKey.get(key);
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
return;
|
return;
|
||||||
@@ -645,7 +750,21 @@ function WorkspaceScreenContent({
|
|||||||
setIsTabSwitcherOpen(false);
|
setIsTabSwitcherOpen(false);
|
||||||
navigateToTab(tab);
|
navigateToTab(tab);
|
||||||
},
|
},
|
||||||
[handleCreateTerminal, navigateToTab, tabByKey]
|
[navigateToTab, tabByKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectNewTabOption = useCallback(
|
||||||
|
(key: string) => {
|
||||||
|
setIsNewTabMenuOpen(false);
|
||||||
|
if (key === NEW_TAB_AGENT_OPTION_ID) {
|
||||||
|
handleCreateAgent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === NEW_TAB_TERMINAL_OPTION_ID) {
|
||||||
|
handleCreateTerminal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleCreateAgent, handleCreateTerminal]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
@@ -653,31 +772,8 @@ function WorkspaceScreenContent({
|
|||||||
return (
|
return (
|
||||||
<View style={styles.emptyState}>
|
<View style={styles.emptyState}>
|
||||||
<Text style={styles.emptyStateText}>
|
<Text style={styles.emptyStateText}>
|
||||||
No agent or terminal tabs are available for this workspace.
|
No tabs are available yet. Use New tab to create an agent or terminal.
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable
|
|
||||||
style={({ hovered, pressed }) => [
|
|
||||||
styles.emptyStateButton,
|
|
||||||
(hovered || pressed) && styles.emptyStateButtonActive,
|
|
||||||
]}
|
|
||||||
onPress={handleCreateAgent}
|
|
||||||
>
|
|
||||||
<Text style={styles.emptyStateButtonText}>Create agent</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
style={({ hovered, pressed }) => [
|
|
||||||
styles.emptyStateButton,
|
|
||||||
(hovered || pressed) && styles.emptyStateButtonActive,
|
|
||||||
]}
|
|
||||||
onPress={handleCreateTerminal}
|
|
||||||
disabled={createTerminalMutation.isPending}
|
|
||||||
>
|
|
||||||
{createTerminalMutation.isPending ? (
|
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
|
||||||
) : (
|
|
||||||
<Text style={styles.emptyStateButtonText}>Create terminal</Text>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -741,16 +837,19 @@ function WorkspaceScreenContent({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="workspace-header-plus"
|
ref={newTabAnchorRef}
|
||||||
|
testID="workspace-new-tab"
|
||||||
style={({ hovered, pressed }) => [
|
style={({ hovered, pressed }) => [
|
||||||
styles.plusButton,
|
styles.newTabButton,
|
||||||
(hovered || pressed) && styles.plusButtonActive,
|
(hovered || pressed) && styles.newTabButtonActive,
|
||||||
]}
|
]}
|
||||||
onPress={handleCreateAgent}
|
onPress={() => setIsNewTabMenuOpen(true)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Create agent"
|
accessibilityLabel="New tab"
|
||||||
>
|
>
|
||||||
<Plus size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
|
<Text style={styles.newTabButtonText}>New tab</Text>
|
||||||
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
@@ -766,6 +865,18 @@ function WorkspaceScreenContent({
|
|||||||
anchorRef={tabSwitcherAnchorRef}
|
anchorRef={tabSwitcherAnchorRef}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<Combobox
|
||||||
|
options={newTabOptions}
|
||||||
|
value=""
|
||||||
|
onSelect={handleSelectNewTabOption}
|
||||||
|
searchable={false}
|
||||||
|
title="New tab"
|
||||||
|
searchPlaceholder="Search tab types"
|
||||||
|
open={isNewTabMenuOpen}
|
||||||
|
onOpenChange={setIsNewTabMenuOpen}
|
||||||
|
anchorRef={newTabAnchorRef}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -780,14 +891,20 @@ function WorkspaceScreenContent({
|
|||||||
>
|
>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = tab.key === activeTabKey;
|
const isActive = tab.key === activeTabKey;
|
||||||
|
const iconColor = isActive
|
||||||
|
? theme.colors.foreground
|
||||||
|
: theme.colors.foregroundMuted;
|
||||||
const icon =
|
const icon =
|
||||||
tab.kind === "agent" ? (
|
tab.kind === "agent" ? (
|
||||||
<AgentStatusDot
|
tab.provider === "claude" ? (
|
||||||
status={agentsById.get(tab.agentId)?.status ?? "idle"}
|
<ClaudeIcon size={14} color={iconColor} />
|
||||||
requiresAttention={agentsById.get(tab.agentId)?.requiresAttention ?? false}
|
) : tab.provider === "codex" ? (
|
||||||
/>
|
<CodexIcon size={14} color={iconColor} />
|
||||||
|
) : (
|
||||||
|
<Bot size={14} color={iconColor} />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Terminal size={14} color={theme.colors.foregroundMuted} />
|
<Terminal size={14} color={iconColor} />
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -820,28 +937,6 @@ function WorkspaceScreenContent({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<Pressable
|
|
||||||
testID="workspace-create-terminal"
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Create terminal"
|
|
||||||
onPress={handleCreateTerminal}
|
|
||||||
disabled={createTerminalMutation.isPending}
|
|
||||||
style={({ hovered, pressed }) => [
|
|
||||||
styles.createTerminalButton,
|
|
||||||
(hovered || pressed) && styles.createTerminalButtonActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{createTerminalMutation.isPending ? (
|
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
|
||||||
) : (
|
|
||||||
<View style={styles.createTerminalIcon}>
|
|
||||||
<Terminal size={16} color={theme.colors.foregroundMuted} />
|
|
||||||
<View style={styles.createTerminalPlusIcon}>
|
|
||||||
<Plus size={10} color={theme.colors.foregroundMuted} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -884,16 +979,23 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
plusButton: {
|
newTabButton: {
|
||||||
padding: {
|
flexDirection: "row",
|
||||||
xs: theme.spacing[3],
|
alignItems: "center",
|
||||||
md: theme.spacing[2],
|
gap: theme.spacing[1],
|
||||||
},
|
paddingHorizontal: theme.spacing[2],
|
||||||
borderRadius: theme.borderRadius.lg,
|
paddingVertical: theme.spacing[1],
|
||||||
|
borderRadius: theme.borderRadius.md,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
plusButtonActive: {
|
newTabButtonActive: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
|
newTabButtonText: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
},
|
||||||
switcherTrigger: {
|
switcherTrigger: {
|
||||||
maxWidth: 220,
|
maxWidth: 220,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -918,8 +1020,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
},
|
||||||
tabsScroll: {
|
tabsScroll: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -962,25 +1062,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
tabLabelActive: {
|
tabLabelActive: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
createTerminalButton: {
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
paddingVertical: theme.spacing[2],
|
|
||||||
marginRight: theme.spacing[2],
|
|
||||||
borderRadius: theme.borderRadius.md,
|
|
||||||
},
|
|
||||||
createTerminalButtonActive: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
createTerminalIcon: {
|
|
||||||
position: "relative",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
createTerminalPlusIcon: {
|
|
||||||
position: "absolute",
|
|
||||||
top: -6,
|
|
||||||
right: -6,
|
|
||||||
},
|
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
@@ -996,18 +1077,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
emptyStateButton: {
|
|
||||||
borderRadius: theme.borderRadius.md,
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
paddingHorizontal: theme.spacing[4],
|
|
||||||
paddingVertical: theme.spacing[2],
|
|
||||||
},
|
|
||||||
emptyStateButtonActive: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
emptyStateButtonText: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontWeight: theme.fontWeight.medium,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user