Skip to content

Commit dae1cdb

Browse files
grichaclaude
andauthored
Add workspace switcher dropdown in mobile navbar (#16)
Tap the workspace name in the detail screen header to show a dropdown with all workspaces. Select one to quickly switch between workspaces without going back to the home screen. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 18c66bc commit dae1cdb

1 file changed

Lines changed: 116 additions & 3 deletions

File tree

mobile/src/screens/WorkspaceDetailScreen.tsx

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
110110
const [agentFilter, setAgentFilter] = useState<AgentType | undefined>(undefined)
111111
const [showAgentPicker, setShowAgentPicker] = useState(false)
112112
const [showNewChatPicker, setShowNewChatPicker] = useState(false)
113+
const [showWorkspacePicker, setShowWorkspacePicker] = useState(false)
113114

114115
const isHost = name === HOST_WORKSPACE_NAME
115116

@@ -126,6 +127,11 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
126127
enabled: isHost,
127128
})
128129

130+
const { data: allWorkspaces } = useQuery({
131+
queryKey: ['workspaces'],
132+
queryFn: api.listWorkspaces,
133+
})
134+
129135
const isRunning = isHost ? true : workspace?.status === 'running'
130136
const isCreating = isHost ? false : workspace?.status === 'creating'
131137

@@ -170,10 +176,14 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
170176
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
171177
<Text style={styles.backBtnText}></Text>
172178
</TouchableOpacity>
173-
<View style={styles.headerCenter}>
174-
<Text style={[styles.headerTitle, isHost && styles.hostHeaderTitle]}>{displayName}</Text>
179+
<TouchableOpacity
180+
style={styles.headerCenter}
181+
onPress={() => setShowWorkspacePicker(!showWorkspacePicker)}
182+
>
183+
<Text style={[styles.headerTitle, isHost && styles.hostHeaderTitle]} numberOfLines={1}>{displayName}</Text>
175184
<View style={[styles.statusIndicator, { backgroundColor: isHost ? '#f59e0b' : (isRunning ? '#34c759' : isCreating ? '#ff9f0a' : '#636366') }]} />
176-
</View>
185+
<Text style={styles.headerChevron}></Text>
186+
</TouchableOpacity>
177187
{isHost ? (
178188
<View style={styles.settingsBtn} />
179189
) : (
@@ -259,6 +269,43 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
259269
</View>
260270
)}
261271

272+
{showWorkspacePicker && (
273+
<View style={styles.workspacePickerOverlay}>
274+
<TouchableOpacity style={styles.workspacePickerBackdrop} onPress={() => setShowWorkspacePicker(false)} />
275+
<View style={styles.workspacePicker}>
276+
<Text style={styles.workspacePickerTitle}>Switch workspace</Text>
277+
{allWorkspaces?.map((ws) => (
278+
<TouchableOpacity
279+
key={ws.name}
280+
style={[styles.workspacePickerItem, ws.name === name && styles.workspacePickerItemActive]}
281+
onPress={() => {
282+
setShowWorkspacePicker(false)
283+
if (ws.name !== name) {
284+
navigation.replace('WorkspaceDetail', { name: ws.name })
285+
}
286+
}}
287+
>
288+
<View style={[styles.workspaceStatusDot, { backgroundColor: ws.status === 'running' ? '#34c759' : ws.status === 'creating' ? '#ff9f0a' : '#636366' }]} />
289+
<Text style={[styles.workspacePickerItemText, ws.name === name && styles.workspacePickerItemTextActive]}>{ws.name}</Text>
290+
{ws.name === name && <Text style={styles.workspaceCheckmark}></Text>}
291+
</TouchableOpacity>
292+
))}
293+
{!isHost && (
294+
<TouchableOpacity
295+
style={styles.workspacePickerItem}
296+
onPress={() => {
297+
setShowWorkspacePicker(false)
298+
navigation.replace('WorkspaceDetail', { name: HOST_WORKSPACE_NAME })
299+
}}
300+
>
301+
<View style={[styles.workspaceStatusDot, { backgroundColor: '#f59e0b' }]} />
302+
<Text style={styles.workspacePickerItemText}>Host Machine</Text>
303+
</TouchableOpacity>
304+
)}
305+
</View>
306+
</View>
307+
)}
308+
262309
{isHost && (
263310
<View style={styles.hostWarningBanner}>
264311
<Text style={styles.hostWarningText}>
@@ -602,4 +649,70 @@ const styles = StyleSheet.create({
602649
fontWeight: '700',
603650
color: '#fff',
604651
},
652+
headerChevron: {
653+
fontSize: 10,
654+
color: '#8e8e93',
655+
marginLeft: 4,
656+
},
657+
workspacePickerOverlay: {
658+
position: 'absolute',
659+
top: 0,
660+
left: 0,
661+
right: 0,
662+
bottom: 0,
663+
zIndex: 100,
664+
},
665+
workspacePickerBackdrop: {
666+
position: 'absolute',
667+
top: 0,
668+
left: 0,
669+
right: 0,
670+
bottom: 0,
671+
backgroundColor: 'rgba(0,0,0,0.5)',
672+
},
673+
workspacePicker: {
674+
position: 'absolute',
675+
top: 60,
676+
left: 50,
677+
right: 50,
678+
backgroundColor: '#2c2c2e',
679+
borderRadius: 12,
680+
padding: 12,
681+
},
682+
workspacePickerTitle: {
683+
fontSize: 13,
684+
fontWeight: '600',
685+
color: '#8e8e93',
686+
marginBottom: 12,
687+
textAlign: 'center',
688+
},
689+
workspacePickerItem: {
690+
flexDirection: 'row',
691+
alignItems: 'center',
692+
paddingVertical: 12,
693+
paddingHorizontal: 12,
694+
borderRadius: 8,
695+
gap: 10,
696+
},
697+
workspacePickerItemActive: {
698+
backgroundColor: '#3c3c3e',
699+
},
700+
workspacePickerItemText: {
701+
fontSize: 16,
702+
color: '#fff',
703+
flex: 1,
704+
},
705+
workspacePickerItemTextActive: {
706+
fontWeight: '600',
707+
},
708+
workspaceStatusDot: {
709+
width: 8,
710+
height: 8,
711+
borderRadius: 4,
712+
},
713+
workspaceCheckmark: {
714+
fontSize: 14,
715+
color: '#0a84ff',
716+
fontWeight: '600',
717+
},
605718
})

0 commit comments

Comments
 (0)