Refresh live worktree display name in mobile session (#5934)

Query the `worktree.show` RPC endpoint on screen focus and poll every
3 seconds to retrieve up-to-date worktree metadata. This ensures that
task-generated names and subsequent displayName updates are reflected,
rather than relying solely on the initial route parameter entry hint.
This commit is contained in:
Jinjing
2026-06-20 16:40:34 -07:00
committed by GitHub
parent fe7cf01c51
commit 8a4f10b35c
4 changed files with 123 additions and 1 deletions
@@ -162,6 +162,7 @@ import {
} from '../../../../src/session/mobile-clipboard-image'
import { useMobileImageAttachment } from '../../../../src/session/use-mobile-image-attachment'
import { classifyMobileArtifact } from '../../../../src/session/mobile-artifact-kind'
import { useLiveWorktreeName } from '../../../../src/session/use-live-worktree-name'
import {
buildMarkdownDiskFallbackDoc,
shouldReadMarkdownFromDiskAfterReadTabFailure
@@ -841,7 +842,7 @@ export default function SessionScreen() {
const {
hostId,
worktreeId,
name: worktreeName,
name: routeWorktreeName,
created,
warning: createdWarning
} = useLocalSearchParams<{
@@ -860,6 +861,12 @@ export default function SessionScreen() {
const reconnectAttempts = useReconnectAttempt(hostId)
const lastConnectedAt = useLastConnectedAt(hostId)
const forceReconnectHost = useForceReconnect()
const worktreeName = useLiveWorktreeName({
client,
connState,
routeName: routeWorktreeName,
worktreeId
})
// Master-detail host state (U5/KTD2): on wide layouts a tapped panel docks beside the
// session content; on narrow it stays null and the icons push full-screen routes.
const { isWideLayout } = useResponsiveLayout()
@@ -0,0 +1,60 @@
import { useCallback, useEffect, useState } from 'react'
import { useFocusEffect } from 'expo-router'
import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState, RpcSuccess } from '../transport/types'
import { getLiveWorktreeDisplayName, type WorktreeDisplayNameSource } from './worktree-display-name'
type Params = {
client: RpcClient | null
connState: ConnectionState
routeName?: string
worktreeId: string
}
export function useLiveWorktreeName({ client, connState, routeName, worktreeId }: Params): string {
const [worktreeName, setWorktreeName] = useState(() => routeName?.trim() ?? '')
useEffect(() => {
setWorktreeName(routeName?.trim() ?? '')
}, [routeName, worktreeId])
useFocusEffect(
useCallback(() => {
if (!client || connState !== 'connected') {
return
}
let stale = false
const refreshWorktreeName = async () => {
try {
const response = await client.sendRequest('worktree.show', {
worktree: `id:${worktreeId}`
})
if (stale || !response.ok) {
return
}
const result = (response as RpcSuccess).result as {
worktree?: WorktreeDisplayNameSource
}
const liveName = result.worktree
? getLiveWorktreeDisplayName([result.worktree], worktreeId)
: null
if (liveName) {
setWorktreeName((current) => (current === liveName ? current : liveName))
}
} catch {
// Non-fatal: the route param remains a usable label until the next refresh.
}
}
// Why: route params are only an entry hint. The desktop/runtime owns
// displayName, including task-generated names that may settle after open.
void refreshWorktreeName()
const interval = setInterval(() => void refreshWorktreeName(), 3000)
return () => {
stale = true
clearInterval(interval)
}
}, [client, connState, worktreeId])
)
return worktreeName
}
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { getLiveWorktreeDisplayName } from './worktree-display-name'
describe('getLiveWorktreeDisplayName', () => {
it('uses the host-list display name for the current worktree', () => {
expect(
getLiveWorktreeDisplayName(
[
{ worktreeId: 'wt-1', displayName: 'Old' },
{ worktreeId: 'wt-2', displayName: 'Auto Generated Name' }
],
'wt-2'
)
).toBe('Auto Generated Name')
})
it('matches worktree.show payloads keyed by id', () => {
expect(getLiveWorktreeDisplayName([{ id: 'wt-1', displayName: 'Settled Name' }], 'wt-1')).toBe(
'Settled Name'
)
})
it('falls back to repo only when the display name is blank', () => {
expect(
getLiveWorktreeDisplayName([{ worktreeId: 'wt-1', displayName: ' ', repo: 'orca' }], 'wt-1')
).toBe('orca')
})
it('ignores snapshots that do not contain the current worktree', () => {
expect(getLiveWorktreeDisplayName([{ worktreeId: 'wt-1', displayName: 'Other' }], 'wt-2')).toBe(
null
)
})
})
@@ -0,0 +1,21 @@
export type WorktreeDisplayNameSource = {
worktreeId?: string
id?: string
displayName?: string | null
repo?: string | null
}
export function getLiveWorktreeDisplayName(
worktrees: readonly WorktreeDisplayNameSource[],
worktreeId: string
): string | null {
const worktree = worktrees.find((item) => (item.worktreeId ?? item.id) === worktreeId)
if (!worktree) {
return null
}
const displayName = worktree.displayName?.trim()
if (displayName) {
return displayName
}
return worktree.repo?.trim() || null
}