Skip to content

Commit aad79cc

Browse files
committed
feat: terminal-first session handling on mobile
Replace chat UI with terminal-based session handling on mobile, matching the web implementation. Sessions now open in Terminal with the correct agent resume command. - Add initialCommand param to TerminalScreen and terminal HTML - Navigate to Terminal instead of SessionChat for sessions - Fix agent CLI commands (claude --resume, opencode --session, codex resume) - Apply same fixes to web WorkspaceDetail
1 parent 808095d commit aad79cc

File tree

6 files changed

+96
-26
lines changed

6 files changed

+96
-26
lines changed

mobile/scripts/bundle-terminal.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ ${umdContent}
4545
let ws = null;
4646
let fitAddon = null;
4747
48-
async function connect(wsUrl) {
48+
async function connect(wsUrl, initialCommand) {
4949
const ghostty = await Ghostty.load();
5050
5151
term = new Terminal({
@@ -102,6 +102,11 @@ ${umdContent}
102102
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'connected' }));
103103
const dims = term.getDimensions ? term.getDimensions() : { cols: term.cols, rows: term.rows };
104104
ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
105+
if (initialCommand) {
106+
setTimeout(() => {
107+
ws.send(initialCommand + '\\n');
108+
}, 500);
109+
}
105110
};
106111
107112
ws.onmessage = (event) => {

mobile/src/lib/demo/terminal-html.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,15 @@ export const DEMO_TERMINAL_HTML = `<!DOCTYPE html>
289289
ctrlActive = !!active;
290290
};
291291
292-
window.initTerminal = () => {
292+
window.initTerminal = (wsUrl, initialCommand) => {
293293
print('Perry demo terminal', 'dim');
294294
print('Type "help" for commands.', 'dim');
295295
renderPrompt();
296296
focusInput();
297297
post({ type: 'connected' });
298+
if (initialCommand) {
299+
setTimeout(() => runCommand(initialCommand), 500);
300+
}
298301
};
299302
300303
function handleIncomingMessage(data) {

mobile/src/lib/terminal-html.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

mobile/src/screens/TerminalScreen.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useTheme } from '../contexts/ThemeContext'
1919
export function TerminalScreen({ route, navigation }: any) {
2020
const insets = useSafeAreaInsets()
2121
const { colors } = useTheme()
22-
const { name } = route.params
22+
const { name, initialCommand, runId: _runId } = route.params
2323
const webViewRef = useRef<WebView>(null)
2424
const [connected, setConnected] = useState(false)
2525
const [loading, setLoading] = useState(true)
@@ -103,10 +103,11 @@ export function TerminalScreen({ route, navigation }: any) {
103103
}
104104

105105
const wsUrl = getTerminalUrl(name)
106+
const escapedCommand = initialCommand ? initialCommand.replace(/\\/g, '\\\\').replace(/'/g, "\\'") : ''
106107

107108
const injectedJS = `
108109
if (window.initTerminal) {
109-
window.initTerminal('${wsUrl}');
110+
window.initTerminal('${wsUrl}', '${escapedCommand}');
110111
}
111112
true;
112113
`

mobile/src/screens/WorkspaceDetailScreen.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,28 @@ import { AgentIcon } from '../components/AgentIcon'
2222

2323
type DateGroup = 'Today' | 'Yesterday' | 'This Week' | 'Older'
2424

25+
function getAgentResumeCommand(agentType: AgentType, sessionId: string): string {
26+
switch (agentType) {
27+
case 'claude-code':
28+
return `claude --resume ${sessionId}`
29+
case 'opencode':
30+
return `opencode --session ${sessionId}`
31+
case 'codex':
32+
return `codex resume ${sessionId}`
33+
}
34+
}
35+
36+
function getAgentNewCommand(agentType: AgentType): string {
37+
switch (agentType) {
38+
case 'claude-code':
39+
return 'claude'
40+
case 'opencode':
41+
return 'opencode'
42+
case 'codex':
43+
return 'codex'
44+
}
45+
}
46+
2547
const DELETE_ACTION_WIDTH = 80
2648

2749
function WorkspaceDetailDeleteAction({
@@ -284,7 +306,7 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
284306
disabled={!isRunning}
285307
testID="new-chat-button"
286308
>
287-
<Text style={[styles.newChatBtnText, { color: colors.accentText }, !isRunning && styles.disabledText]}>New Chat</Text>
309+
<Text style={[styles.newChatBtnText, { color: colors.accentText }, !isRunning && styles.disabledText]}>New Session</Text>
288310
</TouchableOpacity>
289311
</View>
290312
</View>
@@ -320,7 +342,12 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
320342
style={styles.newChatPickerItem}
321343
onPress={() => {
322344
setShowNewChatPicker(false)
323-
navigation.navigate('SessionChat', { workspaceName: name, isNew: true, agentType: type })
345+
const runId = `${type}-${Date.now()}`
346+
navigation.navigate('Terminal', {
347+
name,
348+
initialCommand: getAgentNewCommand(type),
349+
runId,
350+
})
324351
}}
325352
testID={`new-chat-${type}`}
326353
>
@@ -426,13 +453,16 @@ export function WorkspaceDetailScreen({ route, navigation }: any) {
426453
<SessionRow
427454
session={item.session}
428455
colors={colors}
429-
onPress={() => navigation.navigate('SessionChat', {
430-
workspaceName: name,
431-
sessionId: item.session.id,
432-
agentSessionId: item.session.agentSessionId || null,
433-
agentType: item.session.agentType,
434-
projectPath: item.session.projectPath,
435-
})}
456+
onPress={() => {
457+
const resumeId = item.session.agentSessionId || item.session.id
458+
const command = getAgentResumeCommand(item.session.agentType, resumeId)
459+
navigation.navigate('Terminal', {
460+
name,
461+
initialCommand: command,
462+
runId: `${item.session.agentType}-${item.session.id}`,
463+
})
464+
api.recordSessionAccess(name, item.session.id, item.session.agentType).catch(() => {})
465+
}}
436466
/>
437467
</ReanimatedSwipeable>
438468
)

web/src/pages/WorkspaceDetail.tsx

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ const AGENT_LABELS: Record<AgentType | 'all', string> = {
6666

6767
type DateGroup = 'Today' | 'Yesterday' | 'This Week' | 'Older'
6868

69+
function getAgentResumeCommand(agentType: AgentType, sessionId: string): string {
70+
switch (agentType) {
71+
case 'claude-code':
72+
return `claude --resume ${sessionId}`
73+
case 'opencode':
74+
return `opencode --session ${sessionId}`
75+
case 'codex':
76+
return `codex resume ${sessionId}`
77+
}
78+
}
79+
80+
function getAgentNewCommand(agentType: AgentType): string {
81+
switch (agentType) {
82+
case 'claude-code':
83+
return 'claude'
84+
case 'opencode':
85+
return 'opencode'
86+
case 'codex':
87+
return 'codex'
88+
}
89+
}
90+
6991
function getDateGroup(dateString: string): DateGroup {
7092
const date = new Date(dateString)
7193
const now = new Date()
@@ -355,14 +377,11 @@ export function WorkspaceDetail() {
355377

356378
const terminalMode: TerminalMode | null = useMemo(() => {
357379
if (!sessionParam && !agentParam) return null
358-
if (agentParam === 'codex') {
359-
return { type: 'terminal', command: sessionParam ? `codex resume ${sessionParam}` : 'codex', runId: runIdParam ?? undefined }
360-
}
361380
if (agentParam && sessionParam) {
362-
return { type: 'terminal', command: `${agentParam} resume ${sessionParam}` }
381+
return { type: 'terminal', command: getAgentResumeCommand(agentParam as AgentType, sessionParam) }
363382
}
364383
if (agentParam) {
365-
return { type: 'terminal', command: agentParam, runId: runIdParam ?? undefined }
384+
return { type: 'terminal', command: getAgentNewCommand(agentParam as AgentType), runId: runIdParam ?? undefined }
366385
}
367386
return null
368387
}, [sessionParam, agentParam, runIdParam])
@@ -381,13 +400,25 @@ export function WorkspaceDetail() {
381400

382401
setSearchParams((prev) => {
383402
const next = new URLSearchParams(prev)
384-
const resumeMatch = mode.command.match(/^(\S+)\s+resume\s+(\S+)/)
385-
if (resumeMatch) {
386-
next.set('agent', resumeMatch[1])
387-
next.set('session', resumeMatch[2])
403+
const claudeMatch = mode.command.match(/^claude\s+--resume\s+(\S+)/)
404+
const opencodeMatch = mode.command.match(/^opencode\s+--session\s+(\S+)/)
405+
const codexMatch = mode.command.match(/^codex\s+resume\s+(\S+)/)
406+
if (claudeMatch) {
407+
next.set('agent', 'claude-code')
408+
next.set('session', claudeMatch[1])
409+
next.delete('runId')
410+
} else if (opencodeMatch) {
411+
next.set('agent', 'opencode')
412+
next.set('session', opencodeMatch[1])
413+
next.delete('runId')
414+
} else if (codexMatch) {
415+
next.set('agent', 'codex')
416+
next.set('session', codexMatch[1])
388417
next.delete('runId')
389418
} else {
390-
next.set('agent', mode.command)
419+
const agentMap: Record<string, AgentType> = { claude: 'claude-code', opencode: 'opencode', codex: 'codex' }
420+
const agent = agentMap[mode.command] || mode.command
421+
next.set('agent', agent)
391422
next.delete('session')
392423
if (mode.runId) {
393424
next.set('runId', mode.runId)
@@ -535,7 +566,7 @@ export function WorkspaceDetail() {
535566

536567
const handleResume = (session: SessionInfo) => {
537568
const resumeId = session.agentSessionId || session.id
538-
setTerminalMode({ type: 'terminal', command: `${session.agentType} resume ${resumeId}` })
569+
setTerminalMode({ type: 'terminal', command: getAgentResumeCommand(session.agentType, resumeId) })
539570
if (name) {
540571
api.recordSessionAccess(name, session.id, session.agentType).catch(() => {})
541572
queryClient.invalidateQueries({ queryKey: ['sessions', name] })
@@ -544,7 +575,7 @@ export function WorkspaceDetail() {
544575

545576
const handleNewSession = (agentType: AgentType = 'claude-code') => {
546577
const sessionId = `${agentType}-${Date.now()}`
547-
setTerminalMode({ type: 'terminal', command: agentType, runId: sessionId })
578+
setTerminalMode({ type: 'terminal', command: getAgentNewCommand(agentType), runId: sessionId })
548579
}
549580

550581
if (isLoading) {

0 commit comments

Comments
 (0)