From cb326cb632bbca343b69dea1696ace7c7cc9a622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Sat, 19 Sep 2026 05:01:10 +0000 Subject: [PATCH] fix(mobile): keep long session titles on one header line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explorer finding: new-task-started: The session title in the header wraps mid-word and truncates to "Moving-ave / rage empt…". The user-agent explorer found this while using the app like a user. One finding per item; the explorer never edits product code. Flow: new-task-started Found on revision: fd04ca1ef Repro: 1. set this state first: account e2e-mobile-cloud-android@example.com, $10 credits, 3 Code Reviewer pull requests, one top-level busy session (ses_f488ff9c9ffefhXjlZMDy7Ix8y), display 720x1600, day mode, font 1.0, portrait, **event-service down**. 2. open the app on emulator-5554 3. reach the screen the capture names (new-task-started) 4. the capture shows the defect named below Observed: The session title in the header wraps mid-word and truncates to "Moving-ave / rage empt…". Expected: the screen renders without this defect Evidence (from the device run): - ~/.local/share/kwf/findings/explorer-new-task-started-the-session-title-in-the-header-a5387799/new-task-started.png Production record (automatic, 2026-09-19): ## Production record for `ses_f488ff9c9ffefhXjlZMDy7Ix8y` ### Axiom, last 72 h No line carries this term in: `vercel`, `cloudflare-logpush`, `supabase-production`. ### The session row, read replica No row in `cli_sessions_v2` carries `ses_f488ff9c9ffefhXjlZMDy7Ix8y`. ### Log archives, R2 `kilocode-sessions` NOT READ: R2 keys are indexed by the agent id, not the session id; take the agent_* from the worker logs above and query that. ### Sentry, la --- .../agents/session-detail-content.test.ts | 27 ++++++++++++++++--- .../agents/session-detail-content.tsx | 1 + 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/components/agents/session-detail-content.test.ts b/apps/mobile/src/components/agents/session-detail-content.test.ts index 57b87b0bed..48eff0b4f8 100644 --- a/apps/mobile/src/components/agents/session-detail-content.test.ts +++ b/apps/mobile/src/components/agents/session-detail-content.test.ts @@ -561,6 +561,10 @@ function page( // resolves with this cursor instead of the default `null`. let rootPageNextCursor: string | null = null; +// Set before `mountDetails` by the long-title header tests; `fetchSession` +// reports this session title instead of the short default. +let sessionTitleOverride: string | null = null; + function messageLists(renderer: ReactTestRenderer): ReactTestInstance[] { return renderer.root.findAll(node => Object.is(node.type, 'MessageList')); } @@ -575,6 +579,7 @@ beforeEach(() => { globalContext.organizationId = 'global-org'; globalContext.setOrganizationId.mockClear(); rootPageNextCursor = null; + sessionTitleOverride = null; condensePreference.value = false; connectionHealth.isConnected = true; connectionHealth.reconnectExhausted = false; @@ -662,7 +667,7 @@ async function mountDetails( return { kiloSessionId: id, cloudAgentSessionId: null, - title: `Root ${id}`, + title: sessionTitleOverride ?? `Root ${id}`, organizationId: null, gitUrl: null, gitBranch: null, @@ -831,7 +836,7 @@ describe('SessionDetailContent display scope', () => { }); const header = renderer.root.findByType(ScreenHeader); expect(header.findByProps({ accessibilityRole: 'header' }).props).toMatchObject({ - numberOfLines: 2, + numberOfLines: 1, ellipsizeMode: 'tail', }); expect(header.findByProps({ accessibilityRole: 'header' }).parent?.props.className).toContain( @@ -857,6 +862,22 @@ describe('SessionDetailContent display scope', () => { }); }); +describe('SessionDetailContent header title', () => { + // The title shares its row with a 44pt context pill and a copy action, so on + // a narrow phone the title column is a fraction of the row width. Letting the + // Text wrap there split a long word across two lines and truncated the second + // ("Moving-ave / rage empt…"). One line keeps the truncation at a clean tail + // ellipsis instead of breaking a word across two lines. + it('keeps a long session title on one line instead of breaking a word across two', async () => { + sessionTitleOverride = 'Moving-average rage empty baseline'; + const { renderer } = await mountDetails(); + const header = renderer.root.findByType(ScreenHeader); + const title = header.findByProps({ accessibilityRole: 'header' }); + expect(title.props.numberOfLines).toBe(1); + expect(title.props.ellipsizeMode).toBe('tail'); + }); +}); + describe('session detail status placement', () => { it.each(['progress', 'info'] as const)( 'centers a %s status without transcript rows', @@ -1445,7 +1466,7 @@ describe.each([true, false])('session detail return with history=%s', hasHistory const header = view.renderer.root.findByType(ScreenHeader); expect(header.findByProps({ accessibilityRole: 'header' }).props).toMatchObject({ - numberOfLines: 2, + numberOfLines: 1, ellipsizeMode: 'tail', }); expect(header.findByProps({ accessibilityRole: 'header' }).parent?.props.className).toContain( diff --git a/apps/mobile/src/components/agents/session-detail-content.tsx b/apps/mobile/src/components/agents/session-detail-content.tsx index 51deba11ff..a4a614d584 100644 --- a/apps/mobile/src/components/agents/session-detail-content.tsx +++ b/apps/mobile/src/components/agents/session-detail-content.tsx @@ -1876,6 +1876,7 @@ export function SessionDetailContent({ ) : null}