From 8a4f10b35cd857e3631eca0992afd3985417dd69 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:40:34 -0700 Subject: [PATCH] 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. --- .../app/h/[hostId]/session/[worktreeId].tsx | 9 ++- mobile/src/session/use-live-worktree-name.ts | 60 +++++++++++++++++++ .../src/session/worktree-display-name.test.ts | 34 +++++++++++ mobile/src/session/worktree-display-name.ts | 21 +++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 mobile/src/session/use-live-worktree-name.ts create mode 100644 mobile/src/session/worktree-display-name.test.ts create mode 100644 mobile/src/session/worktree-display-name.ts diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx index a86f2971852..76eac77ec55 100644 --- a/mobile/app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx @@ -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() diff --git a/mobile/src/session/use-live-worktree-name.ts b/mobile/src/session/use-live-worktree-name.ts new file mode 100644 index 00000000000..665fcea3a1e --- /dev/null +++ b/mobile/src/session/use-live-worktree-name.ts @@ -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 +} diff --git a/mobile/src/session/worktree-display-name.test.ts b/mobile/src/session/worktree-display-name.test.ts new file mode 100644 index 00000000000..64a01e96236 --- /dev/null +++ b/mobile/src/session/worktree-display-name.test.ts @@ -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 + ) + }) +}) diff --git a/mobile/src/session/worktree-display-name.ts b/mobile/src/session/worktree-display-name.ts new file mode 100644 index 00000000000..c1f6e7079f7 --- /dev/null +++ b/mobile/src/session/worktree-display-name.ts @@ -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 +}