Refactor project opening and add status bar tooltips
This commit is contained in:
@@ -1,5 +1,17 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { normalizeModelId, resolveAgentModelSelection } from './agent-status-bar.utils'
|
import {
|
||||||
|
getStatusSelectorHint,
|
||||||
|
normalizeModelId,
|
||||||
|
resolveAgentModelSelection,
|
||||||
|
} from './agent-status-bar.utils'
|
||||||
|
|
||||||
|
describe('getStatusSelectorHint', () => {
|
||||||
|
it('explains what each editable status control does', () => {
|
||||||
|
expect(getStatusSelectorHint('thinking')).toBe('Thinking mode')
|
||||||
|
expect(getStatusSelectorHint('model')).toBe('Change model')
|
||||||
|
expect(getStatusSelectorHint('mode')).toBe('Change permission mode')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('normalizeModelId', () => {
|
describe('normalizeModelId', () => {
|
||||||
it('treats empty and default values as unset', () => {
|
it('treats empty and default values as unset', () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { Combobox, ComboboxItem, type ComboboxOption } from '@/components/ui/combobox'
|
import { Combobox, ComboboxItem, type ComboboxOption } from '@/components/ui/combobox'
|
||||||
import { AdaptiveModalSheet } from '@/components/adaptive-modal-sheet'
|
import { AdaptiveModalSheet } from '@/components/adaptive-modal-sheet'
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import type {
|
import type {
|
||||||
AgentMode,
|
AgentMode,
|
||||||
AgentModelDefinition,
|
AgentModelDefinition,
|
||||||
@@ -33,13 +34,18 @@ import {
|
|||||||
type AgentModeColorTier,
|
type AgentModeColorTier,
|
||||||
type AgentModeIcon,
|
type AgentModeIcon,
|
||||||
} from '@server/server/agent/provider-manifest'
|
} from '@server/server/agent/provider-manifest'
|
||||||
import { normalizeModelId, resolveAgentModelSelection } from '@/components/agent-status-bar.utils'
|
import {
|
||||||
|
getStatusSelectorHint,
|
||||||
|
resolveAgentModelSelection,
|
||||||
|
} from '@/components/agent-status-bar.utils'
|
||||||
|
|
||||||
type StatusOption = {
|
type StatusOption = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatusSelector = 'provider' | 'mode' | 'model' | 'thinking'
|
||||||
|
|
||||||
type ControlledAgentStatusBarProps = {
|
type ControlledAgentStatusBarProps = {
|
||||||
provider: string
|
provider: string
|
||||||
providerOptions?: StatusOption[]
|
providerOptions?: StatusOption[]
|
||||||
@@ -139,7 +145,7 @@ function ControlledStatusBar({
|
|||||||
const { theme } = useUnistyles()
|
const { theme } = useUnistyles()
|
||||||
const isWeb = Platform.OS === 'web'
|
const isWeb = Platform.OS === 'web'
|
||||||
const [prefsOpen, setPrefsOpen] = useState(false)
|
const [prefsOpen, setPrefsOpen] = useState(false)
|
||||||
const [openSelector, setOpenSelector] = useState<'provider' | 'mode' | 'model' | 'thinking' | null>(null)
|
const [openSelector, setOpenSelector] = useState<StatusSelector | null>(null)
|
||||||
|
|
||||||
const providerAnchorRef = useRef<View>(null)
|
const providerAnchorRef = useRef<View>(null)
|
||||||
const modeAnchorRef = useRef<View>(null)
|
const modeAnchorRef = useRef<View>(null)
|
||||||
@@ -215,12 +221,19 @@ function ControlledStatusBar({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(selector: 'provider' | 'mode' | 'model' | 'thinking') => (nextOpen: boolean) => {
|
(selector: StatusSelector) => (nextOpen: boolean) => {
|
||||||
setOpenSelector(nextOpen ? selector : null)
|
setOpenSelector(nextOpen ? selector : null)
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleSelectorPress = useCallback(
|
||||||
|
(selector: StatusSelector) => {
|
||||||
|
handleOpenChange(selector)(openSelector !== selector)
|
||||||
|
},
|
||||||
|
[handleOpenChange, openSelector]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{isWeb ? (
|
{isWeb ? (
|
||||||
@@ -231,7 +244,7 @@ function ControlledStatusBar({
|
|||||||
ref={providerAnchorRef}
|
ref={providerAnchorRef}
|
||||||
collapsable={false}
|
collapsable={false}
|
||||||
disabled={disabled || !canSelectProvider}
|
disabled={disabled || !canSelectProvider}
|
||||||
onPress={() => setOpenSelector(openSelector === 'provider' ? null : 'provider')}
|
onPress={() => handleSelectorPress('provider')}
|
||||||
style={({ pressed, hovered }) => [
|
style={({ pressed, hovered }) => [
|
||||||
styles.modeBadge,
|
styles.modeBadge,
|
||||||
hovered && styles.modeBadgeHovered,
|
hovered && styles.modeBadgeHovered,
|
||||||
@@ -260,27 +273,39 @@ function ControlledStatusBar({
|
|||||||
|
|
||||||
{modeOptions && modeOptions.length > 0 ? (
|
{modeOptions && modeOptions.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Pressable
|
<Tooltip
|
||||||
ref={modeAnchorRef}
|
key={`mode-${openSelector === 'mode' ? 'open' : 'closed'}`}
|
||||||
collapsable={false}
|
delayDuration={0}
|
||||||
disabled={disabled || !canSelectMode}
|
enabledOnDesktop
|
||||||
onPress={() => setOpenSelector(openSelector === 'mode' ? null : 'mode')}
|
enabledOnMobile={false}
|
||||||
style={({ pressed, hovered }) => [
|
|
||||||
styles.modeIconBadge,
|
|
||||||
hovered && styles.modeBadgeHovered,
|
|
||||||
(pressed || openSelector === 'mode') && styles.modeBadgePressed,
|
|
||||||
(disabled || !canSelectMode) && styles.disabledBadge,
|
|
||||||
]}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={`Select agent mode (${displayMode})`}
|
|
||||||
testID="agent-mode-selector"
|
|
||||||
>
|
>
|
||||||
{ModeIconComponent ? (
|
<TooltipTrigger asChild triggerRefProp="ref">
|
||||||
<ModeIconComponent size={theme.iconSize.md} color={modeIconColor} />
|
<Pressable
|
||||||
) : (
|
ref={modeAnchorRef}
|
||||||
<ShieldCheck size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
collapsable={false}
|
||||||
)}
|
disabled={disabled || !canSelectMode}
|
||||||
</Pressable>
|
onPress={() => handleSelectorPress('mode')}
|
||||||
|
style={({ pressed, hovered }) => [
|
||||||
|
styles.modeIconBadge,
|
||||||
|
hovered && styles.modeBadgeHovered,
|
||||||
|
(pressed || openSelector === 'mode') && styles.modeBadgePressed,
|
||||||
|
(disabled || !canSelectMode) && styles.disabledBadge,
|
||||||
|
]}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Select agent mode (${displayMode})`}
|
||||||
|
testID="agent-mode-selector"
|
||||||
|
>
|
||||||
|
{ModeIconComponent ? (
|
||||||
|
<ModeIconComponent size={theme.iconSize.md} color={modeIconColor} />
|
||||||
|
) : (
|
||||||
|
<ShieldCheck size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
|
<Text style={styles.tooltipText}>{getStatusSelectorHint('mode')}</Text>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={comboboxModeOptions}
|
options={comboboxModeOptions}
|
||||||
value={selectedModeId ?? ''}
|
value={selectedModeId ?? ''}
|
||||||
@@ -297,25 +322,37 @@ function ControlledStatusBar({
|
|||||||
|
|
||||||
{canSelectModel ? (
|
{canSelectModel ? (
|
||||||
<>
|
<>
|
||||||
<Pressable
|
<Tooltip
|
||||||
ref={modelAnchorRef}
|
key={`model-${openSelector === 'model' ? 'open' : 'closed'}`}
|
||||||
collapsable={false}
|
delayDuration={0}
|
||||||
disabled={modelDisabled}
|
enabledOnDesktop
|
||||||
onPress={() => setOpenSelector(openSelector === 'model' ? null : 'model')}
|
enabledOnMobile={false}
|
||||||
style={({ pressed, hovered }) => [
|
|
||||||
styles.modeBadge,
|
|
||||||
hovered && styles.modeBadgeHovered,
|
|
||||||
(pressed || openSelector === 'model') && styles.modeBadgePressed,
|
|
||||||
modelDisabled && styles.disabledBadge,
|
|
||||||
]}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Select agent model"
|
|
||||||
testID="agent-model-selector"
|
|
||||||
>
|
>
|
||||||
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
<TooltipTrigger asChild triggerRefProp="ref">
|
||||||
<Text style={styles.modeBadgeText}>{displayModel}</Text>
|
<Pressable
|
||||||
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
ref={modelAnchorRef}
|
||||||
</Pressable>
|
collapsable={false}
|
||||||
|
disabled={modelDisabled}
|
||||||
|
onPress={() => handleSelectorPress('model')}
|
||||||
|
style={({ pressed, hovered }) => [
|
||||||
|
styles.modeBadge,
|
||||||
|
hovered && styles.modeBadgeHovered,
|
||||||
|
(pressed || openSelector === 'model') && styles.modeBadgePressed,
|
||||||
|
modelDisabled && styles.disabledBadge,
|
||||||
|
]}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Select agent model"
|
||||||
|
testID="agent-model-selector"
|
||||||
|
>
|
||||||
|
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
|
<Text style={styles.modeBadgeText}>{displayModel}</Text>
|
||||||
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
|
</Pressable>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
|
<Text style={styles.tooltipText}>{getStatusSelectorHint('model')}</Text>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={comboboxModelOptions}
|
options={comboboxModelOptions}
|
||||||
value={selectedModelId ?? ''}
|
value={selectedModelId ?? ''}
|
||||||
@@ -331,25 +368,37 @@ function ControlledStatusBar({
|
|||||||
|
|
||||||
{thinkingOptions && thinkingOptions.length > 0 ? (
|
{thinkingOptions && thinkingOptions.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Pressable
|
<Tooltip
|
||||||
ref={thinkingAnchorRef}
|
key={`thinking-${openSelector === 'thinking' ? 'open' : 'closed'}`}
|
||||||
collapsable={false}
|
delayDuration={0}
|
||||||
disabled={disabled || !canSelectThinking}
|
enabledOnDesktop
|
||||||
onPress={() => setOpenSelector(openSelector === 'thinking' ? null : 'thinking')}
|
enabledOnMobile={false}
|
||||||
style={({ pressed, hovered }) => [
|
|
||||||
styles.modeBadge,
|
|
||||||
hovered && styles.modeBadgeHovered,
|
|
||||||
(pressed || openSelector === 'thinking') && styles.modeBadgePressed,
|
|
||||||
(disabled || !canSelectThinking) && styles.disabledBadge,
|
|
||||||
]}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={`Select thinking option (${displayThinking})`}
|
|
||||||
testID="agent-thinking-selector"
|
|
||||||
>
|
>
|
||||||
<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
<TooltipTrigger asChild triggerRefProp="ref">
|
||||||
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
|
<Pressable
|
||||||
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
ref={thinkingAnchorRef}
|
||||||
</Pressable>
|
collapsable={false}
|
||||||
|
disabled={disabled || !canSelectThinking}
|
||||||
|
onPress={() => handleSelectorPress('thinking')}
|
||||||
|
style={({ pressed, hovered }) => [
|
||||||
|
styles.modeBadge,
|
||||||
|
hovered && styles.modeBadgeHovered,
|
||||||
|
(pressed || openSelector === 'thinking') && styles.modeBadgePressed,
|
||||||
|
(disabled || !canSelectThinking) && styles.disabledBadge,
|
||||||
|
]}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Select thinking option (${displayThinking})`}
|
||||||
|
testID="agent-thinking-selector"
|
||||||
|
>
|
||||||
|
<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
|
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
|
||||||
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
|
</Pressable>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
|
<Text style={styles.tooltipText}>{getStatusSelectorHint('thinking')}</Text>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={comboboxThinkingOptions}
|
options={comboboxThinkingOptions}
|
||||||
value={selectedThinkingOptionId ?? ''}
|
value={selectedThinkingOptionId ?? ''}
|
||||||
@@ -386,7 +435,10 @@ function ControlledStatusBar({
|
|||||||
>
|
>
|
||||||
{providerOptions && providerOptions.length > 0 ? (
|
{providerOptions && providerOptions.length > 0 ? (
|
||||||
<View style={styles.sheetSection}>
|
<View style={styles.sheetSection}>
|
||||||
<DropdownMenu>
|
<DropdownMenu
|
||||||
|
open={openSelector === 'provider'}
|
||||||
|
onOpenChange={handleOpenChange('provider')}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
disabled={disabled || !canSelectProvider}
|
disabled={disabled || !canSelectProvider}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
@@ -418,7 +470,10 @@ function ControlledStatusBar({
|
|||||||
|
|
||||||
{modeOptions && modeOptions.length > 0 ? (
|
{modeOptions && modeOptions.length > 0 ? (
|
||||||
<View style={styles.sheetSection}>
|
<View style={styles.sheetSection}>
|
||||||
<DropdownMenu>
|
<DropdownMenu
|
||||||
|
open={openSelector === 'mode'}
|
||||||
|
onOpenChange={handleOpenChange('mode')}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
disabled={disabled || !canSelectMode}
|
disabled={disabled || !canSelectMode}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
@@ -458,7 +513,10 @@ function ControlledStatusBar({
|
|||||||
|
|
||||||
{canSelectModel ? (
|
{canSelectModel ? (
|
||||||
<View style={styles.sheetSection}>
|
<View style={styles.sheetSection}>
|
||||||
<DropdownMenu>
|
<DropdownMenu
|
||||||
|
open={openSelector === 'model'}
|
||||||
|
onOpenChange={handleOpenChange('model')}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
disabled={modelDisabled}
|
disabled={modelDisabled}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
@@ -490,7 +548,10 @@ function ControlledStatusBar({
|
|||||||
|
|
||||||
{thinkingOptions && thinkingOptions.length > 0 ? (
|
{thinkingOptions && thinkingOptions.length > 0 ? (
|
||||||
<View style={styles.sheetSection}>
|
<View style={styles.sheetSection}>
|
||||||
<DropdownMenu>
|
<DropdownMenu
|
||||||
|
open={openSelector === 'thinking'}
|
||||||
|
onOpenChange={handleOpenChange('thinking')}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
disabled={disabled || !canSelectThinking}
|
disabled={disabled || !canSelectThinking}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
@@ -763,6 +824,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
},
|
},
|
||||||
|
tooltipText: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
lineHeight: theme.fontSize.sm * 1.4,
|
||||||
|
},
|
||||||
prefsButton: {
|
prefsButton: {
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import type { AgentModelDefinition } from '@server/server/agent/agent-sdk-types'
|
import type { AgentModelDefinition } from '@server/server/agent/agent-sdk-types'
|
||||||
|
|
||||||
|
export type ExplainedStatusSelector = 'mode' | 'model' | 'thinking'
|
||||||
|
|
||||||
|
export function getStatusSelectorHint(selector: ExplainedStatusSelector): string {
|
||||||
|
switch (selector) {
|
||||||
|
case 'thinking':
|
||||||
|
return 'Thinking mode'
|
||||||
|
case 'model':
|
||||||
|
return 'Change model'
|
||||||
|
case 'mode':
|
||||||
|
return 'Change permission mode'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeModelId(modelId: string | null | undefined): string | null {
|
export function normalizeModelId(modelId: string | null | undefined): string | null {
|
||||||
const normalized = typeof modelId === 'string' ? modelId.trim() : ''
|
const normalized = typeof modelId === 'string' ? modelId.trim() : ''
|
||||||
if (!normalized || normalized.toLowerCase() === 'default') {
|
if (!normalized || normalized.toLowerCase() === 'default') {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
mapPathnameToServer,
|
mapPathnameToServer,
|
||||||
parseServerIdFromPathname,
|
parseServerIdFromPathname,
|
||||||
} from '@/utils/host-routes'
|
} from '@/utils/host-routes'
|
||||||
import { useKeyboardShortcutsStore } from '@/stores/keyboard-shortcuts-store'
|
import { useOpenProjectPicker } from '@/hooks/use-open-project-picker'
|
||||||
|
|
||||||
const DESKTOP_SIDEBAR_WIDTH = 320
|
const DESKTOP_SIDEBAR_WIDTH = 320
|
||||||
|
|
||||||
@@ -154,16 +154,16 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
|
|||||||
closeToAgent()
|
closeToAgent()
|
||||||
}, [closeToAgent])
|
}, [closeToAgent])
|
||||||
|
|
||||||
const setProjectPickerOpen = useKeyboardShortcutsStore((s) => s.setProjectPickerOpen)
|
const openProjectPicker = useOpenProjectPicker(activeServerId)
|
||||||
|
|
||||||
const handleOpenProjectMobile = useCallback(() => {
|
const handleOpenProjectMobile = useCallback(() => {
|
||||||
closeToAgent()
|
closeToAgent()
|
||||||
setProjectPickerOpen(true)
|
void openProjectPicker()
|
||||||
}, [closeToAgent, setProjectPickerOpen])
|
}, [closeToAgent, openProjectPicker])
|
||||||
|
|
||||||
const handleOpenProjectDesktop = useCallback(() => {
|
const handleOpenProjectDesktop = useCallback(() => {
|
||||||
setProjectPickerOpen(true)
|
void openProjectPicker()
|
||||||
}, [setProjectPickerOpen])
|
}, [openProjectPicker])
|
||||||
|
|
||||||
// Mobile: close sidebar and navigate
|
// Mobile: close sidebar and navigate
|
||||||
const handleSettingsMobile = useCallback(() => {
|
const handleSettingsMobile = useCallback(() => {
|
||||||
|
|||||||
@@ -11,22 +11,17 @@ import {
|
|||||||
import { Folder } from "lucide-react-native";
|
import { Folder } from "lucide-react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { router, usePathname } from "expo-router";
|
import { usePathname } from "expo-router";
|
||||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
import { shortenPath } from "@/utils/shorten-path";
|
import { shortenPath } from "@/utils/shorten-path";
|
||||||
import {
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
normalizeWorkspaceDescriptor,
|
|
||||||
useSessionStore,
|
|
||||||
} from "@/stores/session-store";
|
|
||||||
import { useHosts, useHostRuntimeSession } from "@/runtime/host-runtime";
|
import { useHosts, useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||||
import { useToast } from "@/contexts/toast-context";
|
import { useOpenProject } from "@/hooks/use-open-project";
|
||||||
import { parseServerIdFromPathname } from "@/utils/host-routes";
|
import { parseServerIdFromPathname } from "@/utils/host-routes";
|
||||||
import { buildHostWorkspaceRouteWithOpenIntent } from "@/utils/host-routes";
|
|
||||||
import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions";
|
import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions";
|
||||||
|
|
||||||
export function ProjectPickerModal() {
|
export function ProjectPickerModal() {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const toast = useToast();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const daemons = useHosts();
|
const daemons = useHosts();
|
||||||
|
|
||||||
@@ -43,15 +38,12 @@ export function ProjectPickerModal() {
|
|||||||
const workspaces = useSessionStore((state) =>
|
const workspaces = useSessionStore((state) =>
|
||||||
serverId ? state.sessions[serverId]?.workspaces : undefined
|
serverId ? state.sessions[serverId]?.workspaces : undefined
|
||||||
);
|
);
|
||||||
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
|
|
||||||
const setHasHydratedWorkspaces = useSessionStore(
|
|
||||||
(state) => state.setHasHydratedWorkspaces
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const openProject = useOpenProject(serverId);
|
||||||
|
|
||||||
const recommendedPaths = useMemo(() => {
|
const recommendedPaths = useMemo(() => {
|
||||||
if (!workspaces) return [];
|
if (!workspaces) return [];
|
||||||
@@ -102,31 +94,15 @@ export function ProjectPickerModal() {
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const payload = await client.openProject(trimmed);
|
const didOpenProject = await openProject(trimmed);
|
||||||
if (payload.error || !payload.workspace) {
|
if (didOpenProject) {
|
||||||
throw new Error(payload.error || "Failed to open project");
|
setOpen(false);
|
||||||
}
|
}
|
||||||
mergeWorkspaces(serverId, [
|
|
||||||
normalizeWorkspaceDescriptor(payload.workspace),
|
|
||||||
]);
|
|
||||||
setHasHydratedWorkspaces(serverId, true);
|
|
||||||
setOpen(false);
|
|
||||||
router.replace(
|
|
||||||
buildHostWorkspaceRouteWithOpenIntent(
|
|
||||||
serverId,
|
|
||||||
payload.workspace.id,
|
|
||||||
{ kind: "draft", draftId: "new" }
|
|
||||||
) as any
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
error instanceof Error ? error.message : "Failed to open project"
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[client, mergeWorkspaces, serverId, setHasHydratedWorkspaces, setOpen, toast]
|
[client, openProject, serverId, setOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmitCustom = useCallback(() => {
|
const handleSubmitCustom = useCallback(() => {
|
||||||
|
|||||||
@@ -289,13 +289,13 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
maxWidth: "92%",
|
maxWidth: "92%",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[3],
|
gap: theme.spacing[2],
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
borderWidth: theme.borderWidth[1],
|
borderWidth: theme.borderWidth[1],
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
paddingVertical: theme.spacing[3],
|
paddingVertical: theme.spacing[2],
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[3],
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: { width: 0, height: 4 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
shadowOpacity: 0.15,
|
shadowOpacity: 0.15,
|
||||||
|
|||||||
@@ -38,9 +38,13 @@ import {
|
|||||||
|
|
||||||
export interface LocalDaemonSectionProps {
|
export interface LocalDaemonSectionProps {
|
||||||
appVersion: string | null
|
appVersion: string | null
|
||||||
|
showLifecycleControls: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
|
export function LocalDaemonSection({
|
||||||
|
appVersion,
|
||||||
|
showLifecycleControls,
|
||||||
|
}: LocalDaemonSectionProps) {
|
||||||
const { theme } = useUnistyles()
|
const { theme } = useUnistyles()
|
||||||
const showSection = shouldUseManagedDesktopDaemon()
|
const showSection = shouldUseManagedDesktopDaemon()
|
||||||
const { settings, updateSettings } = useAppSettings()
|
const { settings, updateSettings } = useAppSettings()
|
||||||
@@ -347,57 +351,61 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
|
|||||||
<Text style={styles.valueSubtext}>{daemonStatusDetailText}</Text>
|
<Text style={styles.valueSubtext}>{daemonStatusDetailText}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.row, styles.rowBorder]}>
|
{showLifecycleControls ? (
|
||||||
<View style={styles.rowContent}>
|
<>
|
||||||
<Text style={styles.rowTitle}>Daemon management</Text>
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
<Text style={styles.hintText}>
|
<View style={styles.rowContent}>
|
||||||
{isDaemonManagementPaused
|
<Text style={styles.rowTitle}>Daemon management</Text>
|
||||||
? 'Paused. The built-in daemon stays stopped until you start it again.'
|
<Text style={styles.hintText}>
|
||||||
: 'Enabled. Paseo can manage the built-in daemon from the desktop app.'}
|
{isDaemonManagementPaused
|
||||||
</Text>
|
? 'Paused. The built-in daemon stays stopped until you start it again.'
|
||||||
</View>
|
: 'Enabled. Paseo can manage the built-in daemon from the desktop app.'}
|
||||||
<Button
|
</Text>
|
||||||
variant="outline"
|
</View>
|
||||||
size="sm"
|
<Button
|
||||||
leftIcon={
|
variant="outline"
|
||||||
isDaemonManagementPaused ? (
|
size="sm"
|
||||||
<Play size={theme.iconSize.sm} color={theme.colors.foreground} />
|
leftIcon={
|
||||||
) : (
|
isDaemonManagementPaused ? (
|
||||||
<Pause size={theme.iconSize.sm} color={theme.colors.foreground} />
|
<Play size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||||
)
|
) : (
|
||||||
}
|
<Pause size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||||
onPress={handleToggleDaemonManagement}
|
)
|
||||||
disabled={isUpdatingDaemonManagement}
|
}
|
||||||
>
|
onPress={handleToggleDaemonManagement}
|
||||||
{isUpdatingDaemonManagement
|
disabled={isUpdatingDaemonManagement}
|
||||||
? isDaemonManagementPaused
|
>
|
||||||
? 'Resuming...'
|
{isUpdatingDaemonManagement
|
||||||
: 'Pausing...'
|
? isDaemonManagementPaused
|
||||||
: isDaemonManagementPaused
|
? 'Resuming...'
|
||||||
? 'Resume'
|
: 'Pausing...'
|
||||||
: 'Pause'}
|
: isDaemonManagementPaused
|
||||||
</Button>
|
? 'Resume'
|
||||||
</View>
|
: 'Pause'}
|
||||||
<View style={[styles.row, styles.rowBorder]}>
|
</Button>
|
||||||
<View style={styles.rowContent}>
|
</View>
|
||||||
<Text style={styles.rowTitle}>{daemonActionLabel}</Text>
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
<Text style={styles.hintText}>{daemonActionMessage}</Text>
|
<View style={styles.rowContent}>
|
||||||
{statusMessage ? <Text style={styles.statusText}>{statusMessage}</Text> : null}
|
<Text style={styles.rowTitle}>{daemonActionLabel}</Text>
|
||||||
</View>
|
<Text style={styles.hintText}>{daemonActionMessage}</Text>
|
||||||
<Button
|
{statusMessage ? <Text style={styles.statusText}>{statusMessage}</Text> : null}
|
||||||
variant="outline"
|
</View>
|
||||||
size="sm"
|
<Button
|
||||||
leftIcon={<RotateCw size={theme.iconSize.sm} color={theme.colors.foreground} />}
|
variant="outline"
|
||||||
onPress={handleUpdateLocalDaemon}
|
size="sm"
|
||||||
disabled={isRestartingDaemon}
|
leftIcon={<RotateCw size={theme.iconSize.sm} color={theme.colors.foreground} />}
|
||||||
>
|
onPress={handleUpdateLocalDaemon}
|
||||||
{isRestartingDaemon
|
disabled={isRestartingDaemon}
|
||||||
? managedStatus?.status === 'running'
|
>
|
||||||
? 'Restarting...'
|
{isRestartingDaemon
|
||||||
: 'Starting...'
|
? managedStatus?.status === 'running'
|
||||||
: daemonActionLabel}
|
? 'Restarting...'
|
||||||
</Button>
|
: 'Starting...'
|
||||||
</View>
|
: daemonActionLabel}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<View style={[styles.row, styles.rowBorder]}>
|
<View style={[styles.row, styles.rowBorder]}>
|
||||||
<View style={styles.rowContent}>
|
<View style={styles.rowContent}>
|
||||||
<Text style={styles.rowTitle}>Command line (CLI)</Text>
|
<Text style={styles.rowTitle}>Command line (CLI)</Text>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { getTauri } from '@/utils/tauri'
|
||||||
|
|
||||||
|
export async function pickDirectory(): Promise<string | null> {
|
||||||
|
const open = getTauri()?.dialog?.open
|
||||||
|
if (typeof open !== 'function') {
|
||||||
|
throw new Error('Tauri dialog open() is unavailable in this environment.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = await open({
|
||||||
|
directory: true,
|
||||||
|
multiple: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selection === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof selection === 'string') {
|
||||||
|
return selection
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unexpected directory picker response.')
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"
|
|||||||
import { useHosts } from "@/runtime/host-runtime";
|
import { useHosts } from "@/runtime/host-runtime";
|
||||||
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
|
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
|
||||||
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
import {
|
import {
|
||||||
clearCommandCenterFocusRestoreElement,
|
clearCommandCenterFocusRestoreElement,
|
||||||
takeCommandCenterFocusRestoreElement,
|
takeCommandCenterFocusRestoreElement,
|
||||||
@@ -217,18 +218,18 @@ export function useCommandCenter() {
|
|||||||
[pathname, setOpen]
|
[pathname, setOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setProjectPickerOpen = useKeyboardShortcutsStore((s) => s.setProjectPickerOpen);
|
const openProjectPicker = useOpenProjectPicker(activeServerId);
|
||||||
|
|
||||||
const handleSelectAction = useCallback((action: CommandCenterActionItem) => {
|
const handleSelectAction = useCallback((action: CommandCenterActionItem) => {
|
||||||
clearCommandCenterFocusRestoreElement();
|
clearCommandCenterFocusRestoreElement();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
if (action.id === "new-agent") {
|
if (action.id === "new-agent") {
|
||||||
setProjectPickerOpen(true);
|
void openProjectPicker();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
didNavigateRef.current = true;
|
didNavigateRef.current = true;
|
||||||
router.push(action.route);
|
router.push(action.route);
|
||||||
}, [setOpen, setProjectPickerOpen]);
|
}, [openProjectPicker, setOpen]);
|
||||||
|
|
||||||
const handleSelectItem = useCallback(
|
const handleSelectItem = useCallback(
|
||||||
(item: CommandCenterItem) => {
|
(item: CommandCenterItem) => {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { getManagedDaemonStatus } from '@/desktop/managed-runtime/managed-runtime'
|
||||||
|
import { isTauriEnvironment } from '@/utils/tauri'
|
||||||
|
|
||||||
|
const MANAGED_DAEMON_SERVER_ID_QUERY_KEY = ['managed-daemon-server-id'] as const
|
||||||
|
|
||||||
|
interface ManagedDaemonServerIdResult {
|
||||||
|
serverId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadManagedDaemonServerId(): Promise<ManagedDaemonServerIdResult> {
|
||||||
|
const status = await getManagedDaemonStatus()
|
||||||
|
const serverId = status.serverId.trim()
|
||||||
|
return {
|
||||||
|
serverId: serverId.length > 0 ? serverId : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsLocalDaemon(serverId: string): boolean {
|
||||||
|
const normalizedServerId = serverId.trim()
|
||||||
|
const isDesktop = isTauriEnvironment()
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: MANAGED_DAEMON_SERVER_ID_QUERY_KEY,
|
||||||
|
queryFn: loadManagedDaemonServerId,
|
||||||
|
enabled: isDesktop,
|
||||||
|
staleTime: Infinity,
|
||||||
|
gcTime: Infinity,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isDesktop || normalizedServerId.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.data?.serverId === normalizedServerId
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ import { useEffect } from "react";
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { usePathname } from "expo-router";
|
import { usePathname } from "expo-router";
|
||||||
import { getIsTauri } from "@/constants/layout";
|
import { getIsTauri } from "@/constants/layout";
|
||||||
|
import { useHosts } from "@/runtime/host-runtime";
|
||||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
|
import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
|
||||||
import {
|
import {
|
||||||
parseHostAgentRouteFromPathname,
|
parseHostAgentRouteFromPathname,
|
||||||
|
parseServerIdFromPathname,
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
|
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
|
||||||
@@ -18,6 +20,7 @@ import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"
|
|||||||
import { resolveKeyboardShortcut } from "@/keyboard/keyboard-shortcuts";
|
import { resolveKeyboardShortcut } from "@/keyboard/keyboard-shortcuts";
|
||||||
import { resolveKeyboardFocusScope } from "@/keyboard/focus-scope";
|
import { resolveKeyboardFocusScope } from "@/keyboard/focus-scope";
|
||||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
|
|
||||||
export function useKeyboardShortcuts({
|
export function useKeyboardShortcuts({
|
||||||
enabled,
|
enabled,
|
||||||
@@ -33,7 +36,14 @@ export function useKeyboardShortcuts({
|
|||||||
toggleFileExplorer?: () => void;
|
toggleFileExplorer?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const hosts = useHosts();
|
||||||
const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers);
|
const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers);
|
||||||
|
const activeServerIdFromPath = parseServerIdFromPathname(pathname);
|
||||||
|
const activeServerId =
|
||||||
|
hosts.find((host) => host.serverId === activeServerIdFromPath)?.serverId ??
|
||||||
|
hosts[0]?.serverId ??
|
||||||
|
null;
|
||||||
|
const openProjectPickerAction = useOpenProjectPicker(activeServerId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
@@ -92,7 +102,7 @@ export function useKeyboardShortcuts({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openProjectPicker = (): boolean => {
|
const openProjectPicker = (): boolean => {
|
||||||
useKeyboardShortcutsStore.getState().setProjectPickerOpen(true);
|
void openProjectPickerAction();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -326,6 +336,7 @@ export function useKeyboardShortcuts({
|
|||||||
}, [
|
}, [
|
||||||
enabled,
|
enabled,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
openProjectPickerAction,
|
||||||
pathname,
|
pathname,
|
||||||
resetModifiers,
|
resetModifiers,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { pickDirectory } from "@/desktop/pick-directory";
|
||||||
|
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
|
import { useIsLocalDaemon } from "./use-is-local-daemon";
|
||||||
|
import { useOpenProject } from "./use-open-project";
|
||||||
|
|
||||||
|
export function useOpenProjectPicker(serverId: string | null): () => Promise<void> {
|
||||||
|
const normalizedServerId = serverId?.trim() ?? "";
|
||||||
|
const isLocalDaemon = useIsLocalDaemon(normalizedServerId);
|
||||||
|
const setProjectPickerOpen = useKeyboardShortcutsStore(
|
||||||
|
(state) => state.setProjectPickerOpen
|
||||||
|
);
|
||||||
|
const openProject = useOpenProject(serverId);
|
||||||
|
|
||||||
|
return useCallback(async () => {
|
||||||
|
if (!normalizedServerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLocalDaemon) {
|
||||||
|
setProjectPickerOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = await pickDirectory();
|
||||||
|
if (path === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await openProject(path);
|
||||||
|
}, [isLocalDaemon, normalizedServerId, openProject, setProjectPickerOpen]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { router } from "expo-router";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useToast } from "@/contexts/toast-context";
|
||||||
|
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||||
|
import {
|
||||||
|
normalizeWorkspaceDescriptor,
|
||||||
|
useSessionStore,
|
||||||
|
} from "@/stores/session-store";
|
||||||
|
import { buildHostWorkspaceRouteWithOpenIntent } from "@/utils/host-routes";
|
||||||
|
|
||||||
|
export function useOpenProject(
|
||||||
|
serverId: string | null
|
||||||
|
): (path: string) => Promise<boolean> {
|
||||||
|
const normalizedServerId = serverId?.trim() ?? "";
|
||||||
|
const toast = useToast();
|
||||||
|
const { client } = useHostRuntimeSession(normalizedServerId);
|
||||||
|
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
|
||||||
|
const setHasHydratedWorkspaces = useSessionStore(
|
||||||
|
(state) => state.setHasHydratedWorkspaces
|
||||||
|
);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async (path: string) => {
|
||||||
|
const trimmedPath = path.trim();
|
||||||
|
if (!trimmedPath || !client || !normalizedServerId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await client.openProject(trimmedPath);
|
||||||
|
if (payload.error || !payload.workspace) {
|
||||||
|
throw new Error(payload.error || "Failed to open project");
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeWorkspaces(normalizedServerId, [
|
||||||
|
normalizeWorkspaceDescriptor(payload.workspace),
|
||||||
|
]);
|
||||||
|
setHasHydratedWorkspaces(normalizedServerId, true);
|
||||||
|
router.replace(
|
||||||
|
buildHostWorkspaceRouteWithOpenIntent(
|
||||||
|
normalizedServerId,
|
||||||
|
payload.workspace.id,
|
||||||
|
{ kind: "draft", draftId: "new" }
|
||||||
|
) as any
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Failed to open project"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
client,
|
||||||
|
mergeWorkspaces,
|
||||||
|
normalizedServerId,
|
||||||
|
setHasHydratedWorkspaces,
|
||||||
|
toast,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,18 +5,18 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|||||||
import { FolderOpen } from "lucide-react-native";
|
import { FolderOpen } from "lucide-react-native";
|
||||||
import { PaseoLogo } from "@/components/icons/paseo-logo";
|
import { PaseoLogo } from "@/components/icons/paseo-logo";
|
||||||
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
||||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { getIsTauriMac } from "@/constants/layout";
|
import { getIsTauriMac } from "@/constants/layout";
|
||||||
import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window";
|
import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window";
|
||||||
|
|
||||||
export function OpenProjectScreen({ serverId: _serverId }: { serverId: string }) {
|
export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const trafficLightPadding = useTrafficLightPadding();
|
const trafficLightPadding = useTrafficLightPadding();
|
||||||
const desktopAgentListOpen = usePanelStore((s) => s.desktop.agentListOpen);
|
const desktopAgentListOpen = usePanelStore((s) => s.desktop.agentListOpen);
|
||||||
const openAgentList = usePanelStore((s) => s.openAgentList);
|
const openAgentList = usePanelStore((s) => s.openAgentList);
|
||||||
const setProjectPickerOpen = useKeyboardShortcutsStore((s) => s.setProjectPickerOpen);
|
const openProjectPicker = useOpenProjectPicker(serverId);
|
||||||
|
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const needsTrafficLightInset = !isMobile && !desktopAgentListOpen && getIsTauriMac();
|
const needsTrafficLightInset = !isMobile && !desktopAgentListOpen && getIsTauriMac();
|
||||||
@@ -42,7 +42,9 @@ export function OpenProjectScreen({ serverId: _serverId }: { serverId: string })
|
|||||||
styles.openButton,
|
styles.openButton,
|
||||||
hovered && styles.openButtonHovered,
|
hovered && styles.openButtonHovered,
|
||||||
]}
|
]}
|
||||||
onPress={() => setProjectPickerOpen(true)}
|
onPress={() => {
|
||||||
|
void openProjectPicker();
|
||||||
|
}}
|
||||||
testID="open-project-submit"
|
testID="open-project-submit"
|
||||||
>
|
>
|
||||||
<FolderOpen size={16} color={theme.colors.foregroundMuted} />
|
<FolderOpen size={16} color={theme.colors.foregroundMuted} />
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { resolveAppVersion } from "@/utils/app-version";
|
|||||||
import { settingsStyles } from "@/styles/settings";
|
import { settingsStyles } from "@/styles/settings";
|
||||||
import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm";
|
import { THINKING_TONE_NATIVE_PCM_BASE64 } from "@/utils/thinking-tone.native-pcm";
|
||||||
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
|
import { useVoiceAudioEngineOptional } from "@/contexts/voice-context";
|
||||||
|
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
|
||||||
|
|
||||||
const delay = (ms: number) =>
|
const delay = (ms: number) =>
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
@@ -513,6 +514,7 @@ export default function SettingsScreen() {
|
|||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
const lastHandledEditHostRef = useRef<string | null>(null);
|
const lastHandledEditHostRef = useRef<string | null>(null);
|
||||||
const isDesktop = Platform.OS === "web";
|
const isDesktop = Platform.OS === "web";
|
||||||
|
const isLocalDaemon = useIsLocalDaemon(routeServerId);
|
||||||
const appVersion = resolveAppVersion();
|
const appVersion = resolveAppVersion();
|
||||||
const appVersionText = formatVersionWithPrefix(appVersion);
|
const appVersionText = formatVersionWithPrefix(appVersion);
|
||||||
const editingServerId = editingDaemon?.serverId ?? null;
|
const editingServerId = editingDaemon?.serverId ?? null;
|
||||||
@@ -906,7 +908,12 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{isDesktop ? <DesktopPermissionsSection /> : null}
|
{isDesktop ? <DesktopPermissionsSection /> : null}
|
||||||
{isDesktop ? <LocalDaemonSection appVersion={appVersion} /> : null}
|
{isDesktop ? (
|
||||||
|
<LocalDaemonSection
|
||||||
|
appVersion={appVersion}
|
||||||
|
showLifecycleControls={isLocalDaemon}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<View style={settingsStyles.section}>
|
<View style={settingsStyles.section}>
|
||||||
<Text style={settingsStyles.sectionTitle}>Diagnostics</Text>
|
<Text style={settingsStyles.sectionTitle}>Diagnostics</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user