Skip to content

Commit 99d944e

Browse files
grichaclaude
andcommitted
Fix mobile terminal for host, add workspace creation, improve UX
- Fix terminal screen to work with host workspace (@host) - Add back button to Settings screen - Add "+" button to create new workspaces from mobile - Add getRecentSessions/recordSessionAccess API endpoints - Reduce session list fetch from 100 to 50 (match web) - Disable keyboard auto-capitalization in terminal - Add react-native-webview dependency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4d54729 commit 99d944e

8 files changed

Lines changed: 249 additions & 23 deletions

File tree

mobile/bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"react-native": "0.81.5",
3131
"react-native-safe-area-context": "^5.6.2",
3232
"react-native-screens": "^4.19.0",
33-
"react-native-webview": "13.15.0"
33+
"react-native-webview": "^13.16.0"
3434
},
3535
"devDependencies": {
3636
"@types/react": "~19.1.0",

mobile/src/lib/api.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ export interface SessionDetail {
8484
messages: SessionMessage[]
8585
}
8686

87+
export interface RecentSession {
88+
workspaceName: string
89+
sessionId: string
90+
agentType: AgentType
91+
lastAccessed: string
92+
}
93+
8794
const DEFAULT_PORT = 7391
8895
const STORAGE_KEY = 'perry_server_config'
8996

@@ -156,6 +163,8 @@ function createClient() {
156163
offset?: number
157164
}) => Promise<{ sessions: (SessionInfo & { workspaceName: string })[]; total: number; hasMore: boolean }>
158165
get: (input: { workspaceName: string; sessionId: string; agentType?: AgentType; limit?: number; offset?: number }) => Promise<SessionDetail & { total: number; hasMore: boolean }>
166+
getRecent: (input: { limit?: number }) => Promise<{ sessions: RecentSession[] }>
167+
recordAccess: (input: { workspaceName: string; sessionId: string; agentType: AgentType }) => Promise<{ success: boolean }>
159168
}
160169
info: () => Promise<InfoResponse>
161170
host: {
@@ -222,6 +231,10 @@ export const api = {
222231
client.sessions.listAll({ agentType, limit, offset }),
223232
getSession: (workspaceName: string, sessionId: string, agentType?: AgentType, limit?: number, offset?: number) =>
224233
client.sessions.get({ workspaceName, sessionId, agentType, limit, offset }),
234+
getRecentSessions: (limit?: number) =>
235+
client.sessions.getRecent({ limit }),
236+
recordSessionAccess: (workspaceName: string, sessionId: string, agentType: AgentType) =>
237+
client.sessions.recordAccess({ workspaceName, sessionId, agentType }),
225238
getInfo: () => client.info(),
226239
getHostInfo: () => client.host.info(),
227240
updateHostAccess: (enabled: boolean) => client.host.updateAccess({ enabled }),

mobile/src/screens/HomeScreen.tsx

Lines changed: 182 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from 'react'
1+
import { useCallback, useState } from 'react'
22
import {
33
View,
44
Text,
@@ -7,11 +7,16 @@ import {
77
StyleSheet,
88
RefreshControl,
99
ActivityIndicator,
10+
Modal,
11+
TextInput,
12+
KeyboardAvoidingView,
13+
Platform,
14+
Alert,
1015
} from 'react-native'
1116
import { useSafeAreaInsets } from 'react-native-safe-area-context'
1217
import { useNavigation } from '@react-navigation/native'
13-
import { useQuery } from '@tanstack/react-query'
14-
import { api, WorkspaceInfo, HOST_WORKSPACE_NAME } from '../lib/api'
18+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
19+
import { api, WorkspaceInfo, HOST_WORKSPACE_NAME, CreateWorkspaceRequest } from '../lib/api'
1520
import { useNetwork, parseNetworkError } from '../lib/network'
1621

1722
function StatusDot({ status }: { status: WorkspaceInfo['status'] | 'host' }) {
@@ -104,13 +109,43 @@ function HostSection({ onHostPress }: { onHostPress: () => void }) {
104109
export function HomeScreen() {
105110
const insets = useSafeAreaInsets()
106111
const navigation = useNavigation<any>()
112+
const queryClient = useQueryClient()
107113
const { status } = useNetwork()
114+
const [showCreate, setShowCreate] = useState(false)
115+
const [newName, setNewName] = useState('')
116+
const [newRepo, setNewRepo] = useState('')
108117

109118
const { data: workspaces, isLoading, refetch, isRefetching, error } = useQuery({
110119
queryKey: ['workspaces'],
111120
queryFn: api.listWorkspaces,
112121
})
113122

123+
const createMutation = useMutation({
124+
mutationFn: (data: CreateWorkspaceRequest) => api.createWorkspace(data),
125+
onSuccess: (workspace) => {
126+
queryClient.invalidateQueries({ queryKey: ['workspaces'] })
127+
setShowCreate(false)
128+
setNewName('')
129+
setNewRepo('')
130+
navigation.navigate('WorkspaceDetail', { name: workspace.name })
131+
},
132+
onError: (err) => {
133+
Alert.alert('Error', parseNetworkError(err))
134+
},
135+
})
136+
137+
const handleCreate = () => {
138+
const name = newName.trim()
139+
if (!name) {
140+
Alert.alert('Error', 'Please enter a workspace name')
141+
return
142+
}
143+
createMutation.mutate({
144+
name,
145+
clone: newRepo.trim() || undefined,
146+
})
147+
}
148+
114149
const handleWorkspacePress = useCallback((workspace: WorkspaceInfo) => {
115150
navigation.navigate('WorkspaceDetail', { name: workspace.name })
116151
}, [navigation])
@@ -150,13 +185,22 @@ export function HomeScreen() {
150185
<View style={[styles.container, { paddingTop: insets.top }]}>
151186
<View style={styles.header}>
152187
<Text style={styles.headerTitle}>Perry</Text>
153-
<TouchableOpacity
154-
style={styles.settingsBtn}
155-
onPress={() => navigation.navigate('Settings')}
156-
testID="settings-button"
157-
>
158-
<Text style={styles.settingsIcon}></Text>
159-
</TouchableOpacity>
188+
<View style={styles.headerButtons}>
189+
<TouchableOpacity
190+
style={styles.headerBtn}
191+
onPress={() => setShowCreate(true)}
192+
testID="add-workspace-button"
193+
>
194+
<Text style={styles.addIcon}>+</Text>
195+
</TouchableOpacity>
196+
<TouchableOpacity
197+
style={styles.headerBtn}
198+
onPress={() => navigation.navigate('Settings')}
199+
testID="settings-button"
200+
>
201+
<Text style={styles.settingsIcon}></Text>
202+
</TouchableOpacity>
203+
</View>
160204
</View>
161205

162206
<FlatList
@@ -179,6 +223,66 @@ export function HomeScreen() {
179223
}
180224
ItemSeparatorComponent={() => <View style={styles.separator} />}
181225
/>
226+
227+
<Modal
228+
visible={showCreate}
229+
animationType="slide"
230+
presentationStyle="pageSheet"
231+
onRequestClose={() => setShowCreate(false)}
232+
>
233+
<KeyboardAvoidingView
234+
style={styles.modalContainer}
235+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
236+
>
237+
<View style={styles.modalHeader}>
238+
<TouchableOpacity onPress={() => setShowCreate(false)} style={styles.modalCancelBtn}>
239+
<Text style={styles.modalCancelText}>Cancel</Text>
240+
</TouchableOpacity>
241+
<Text style={styles.modalTitle}>New Workspace</Text>
242+
<TouchableOpacity
243+
onPress={handleCreate}
244+
style={styles.modalCreateBtn}
245+
disabled={createMutation.isPending || !newName.trim()}
246+
>
247+
{createMutation.isPending ? (
248+
<ActivityIndicator size="small" color="#0a84ff" />
249+
) : (
250+
<Text style={[styles.modalCreateText, !newName.trim() && styles.modalCreateTextDisabled]}>
251+
Create
252+
</Text>
253+
)}
254+
</TouchableOpacity>
255+
</View>
256+
<View style={styles.modalContent}>
257+
<View style={styles.inputGroup}>
258+
<Text style={styles.inputLabel}>Name</Text>
259+
<TextInput
260+
style={styles.modalInput}
261+
value={newName}
262+
onChangeText={setNewName}
263+
placeholder="my-project"
264+
placeholderTextColor="#636366"
265+
autoCapitalize="none"
266+
autoCorrect={false}
267+
autoFocus
268+
/>
269+
</View>
270+
<View style={styles.inputGroup}>
271+
<Text style={styles.inputLabel}>Repository (optional)</Text>
272+
<TextInput
273+
style={styles.modalInput}
274+
value={newRepo}
275+
onChangeText={setNewRepo}
276+
placeholder="https://github.com/user/repo"
277+
placeholderTextColor="#636366"
278+
autoCapitalize="none"
279+
autoCorrect={false}
280+
keyboardType="url"
281+
/>
282+
</View>
283+
</View>
284+
</KeyboardAvoidingView>
285+
</Modal>
182286
</View>
183287
)
184288
}
@@ -207,12 +311,21 @@ const styles = StyleSheet.create({
207311
fontWeight: '700',
208312
color: '#fff',
209313
},
210-
settingsBtn: {
314+
headerButtons: {
315+
flexDirection: 'row',
316+
alignItems: 'center',
317+
},
318+
headerBtn: {
211319
width: 44,
212320
height: 44,
213321
alignItems: 'center',
214322
justifyContent: 'center',
215323
},
324+
addIcon: {
325+
fontSize: 28,
326+
color: '#0a84ff',
327+
fontWeight: '300',
328+
},
216329
settingsIcon: {
217330
fontSize: 22,
218331
color: '#8e8e93',
@@ -358,4 +471,62 @@ const styles = StyleSheet.create({
358471
fontWeight: '600',
359472
color: '#fff',
360473
},
474+
modalContainer: {
475+
flex: 1,
476+
backgroundColor: '#000',
477+
},
478+
modalHeader: {
479+
flexDirection: 'row',
480+
alignItems: 'center',
481+
justifyContent: 'space-between',
482+
paddingHorizontal: 16,
483+
paddingVertical: 12,
484+
borderBottomWidth: 1,
485+
borderBottomColor: '#1c1c1e',
486+
},
487+
modalCancelBtn: {
488+
paddingVertical: 8,
489+
},
490+
modalCancelText: {
491+
fontSize: 17,
492+
color: '#0a84ff',
493+
},
494+
modalTitle: {
495+
fontSize: 17,
496+
fontWeight: '600',
497+
color: '#fff',
498+
},
499+
modalCreateBtn: {
500+
paddingVertical: 8,
501+
minWidth: 60,
502+
alignItems: 'flex-end',
503+
},
504+
modalCreateText: {
505+
fontSize: 17,
506+
fontWeight: '600',
507+
color: '#0a84ff',
508+
},
509+
modalCreateTextDisabled: {
510+
color: '#636366',
511+
},
512+
modalContent: {
513+
padding: 16,
514+
},
515+
inputGroup: {
516+
marginBottom: 20,
517+
},
518+
inputLabel: {
519+
fontSize: 13,
520+
color: '#8e8e93',
521+
marginBottom: 8,
522+
textTransform: 'uppercase',
523+
letterSpacing: 0.5,
524+
},
525+
modalInput: {
526+
backgroundColor: '#1c1c1e',
527+
borderRadius: 10,
528+
padding: 14,
529+
fontSize: 17,
530+
color: '#fff',
531+
},
361532
})

mobile/src/screens/SessionChatScreen.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,11 @@ export function SessionChatScreen({ route, navigation }: any) {
304304
setTimeout(() => {
305305
flatListRef.current?.scrollToEnd({ animated: false })
306306
}, 150)
307+
if (initialSessionId) {
308+
api.recordSessionAccess(workspaceName, initialSessionId, agentType).catch(() => {})
309+
}
307310
}
308-
}, [sessionData, parseMessages])
311+
}, [sessionData, parseMessages, workspaceName, initialSessionId, agentType])
309312

310313
const loadMoreMessages = useCallback(async () => {
311314
if (!hasMoreMessages || isLoadingMore || !initialSessionId) return

mobile/src/screens/SettingsScreen.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ function formatUptime(seconds: number): string {
699699
return `${mins}m`
700700
}
701701

702-
export function SettingsScreen() {
702+
export function SettingsScreen({ navigation }: any) {
703703
const insets = useSafeAreaInsets()
704704

705705
return (
@@ -708,7 +708,11 @@ export function SettingsScreen() {
708708
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
709709
>
710710
<View style={styles.header}>
711+
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
712+
<Text style={styles.backBtnText}></Text>
713+
</TouchableOpacity>
711714
<Text style={styles.headerTitle}>Settings</Text>
715+
<View style={styles.headerPlaceholder} />
712716
</View>
713717
<ScrollView contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 20 }]}>
714718
<ConnectionSettings />
@@ -729,15 +733,33 @@ const styles = StyleSheet.create({
729733
backgroundColor: '#000',
730734
},
731735
header: {
732-
paddingHorizontal: 16,
733-
paddingVertical: 12,
736+
flexDirection: 'row',
737+
alignItems: 'center',
738+
paddingHorizontal: 8,
739+
paddingVertical: 8,
734740
borderBottomWidth: 1,
735741
borderBottomColor: '#1c1c1e',
736742
},
743+
backBtn: {
744+
width: 44,
745+
height: 44,
746+
alignItems: 'center',
747+
justifyContent: 'center',
748+
},
749+
backBtnText: {
750+
fontSize: 32,
751+
color: '#0a84ff',
752+
fontWeight: '300',
753+
},
737754
headerTitle: {
738-
fontSize: 28,
739-
fontWeight: '700',
755+
flex: 1,
756+
fontSize: 17,
757+
fontWeight: '600',
740758
color: '#fff',
759+
textAlign: 'center',
760+
},
761+
headerPlaceholder: {
762+
width: 44,
741763
},
742764
content: {
743765
padding: 16,

0 commit comments

Comments
 (0)