Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/happy-app/sources/-session/SessionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import { voiceHooks } from '@/realtime/hooks/voiceHooks';
import { startRealtimeSession, stopRealtimeSession } from '@/realtime/RealtimeSession';
import { gitStatusSync } from '@/sync/gitStatusSync';
import { sessionAbort } from '@/sync/ops';
import { storage, useIsDataReady, useLocalSetting, useRealtimeStatus, useSessionMessages, useSessionUsage, useSetting } from '@/sync/storage';
import { storage, useIsDataReady, useLocalSetting, useMachine, useRealtimeStatus, useSessionMessages, useSessionUsage, useSetting } from '@/sync/storage';
import { useSession } from '@/sync/storage';
import { Session } from '@/sync/storageTypes';
import { sync } from '@/sync/sync';
import { t } from '@/text';
import { tracking, trackMessageSent } from '@/track';
import { isRunningOnMac } from '@/utils/platform';
import { useDeviceType, useHeaderHeight, useIsLandscape, useIsTablet } from '@/utils/responsive';
import { formatPathRelativeToHome, getSessionAvatarId, getSessionName, useSessionStatus } from '@/utils/sessionUtils';
import { formatPathRelativeToHome, getMachineDisplayName, getSessionAvatarId, getSessionName, useSessionStatus } from '@/utils/sessionUtils';
import { isVersionSupported, MINIMUM_CLI_VERSION } from '@/utils/versionUtils';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
Expand All @@ -51,6 +51,10 @@ export const SessionView = React.memo((props: { id: string }) => {
const realtimeStatus = useRealtimeStatus();
const isTablet = useIsTablet();

// Get machine for display name
const machineId = session?.metadata?.machineId;
const machine = useMachine(machineId || '');

// Compute header props based on session state
const headerProps = useMemo(() => {
if (!isDataReady) {
Expand Down Expand Up @@ -79,16 +83,20 @@ export const SessionView = React.memo((props: { id: string }) => {

// Normal state - show session info
const isConnected = session.presence === 'online';
const formattedPath = session.metadata?.path ? formatPathRelativeToHome(session.metadata.path, session.metadata?.homeDir) : undefined;
const machineDisplayName = getMachineDisplayName(session, machine);
const subtitle = formattedPath && machineDisplayName ? `${machineDisplayName}:${formattedPath}` : formattedPath;

return {
title: getSessionName(session),
subtitle: session.metadata?.path ? formatPathRelativeToHome(session.metadata.path, session.metadata?.homeDir) : undefined,
subtitle,
avatarId: getSessionAvatarId(session),
onAvatarPress: () => router.push(`/session/${sessionId}/info`),
isConnected: isConnected,
flavor: session.metadata?.flavor || null,
tintColor: isConnected ? '#000' : '#8E8E93'
};
}, [session, isDataReady, sessionId, router]);
}, [session, isDataReady, sessionId, router, machine]);

return (
<>
Expand Down
16 changes: 15 additions & 1 deletion packages/happy-app/sources/components/ActiveSessionsGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Text } from '@/components/StyledText';
import { useRouter } from 'expo-router';
import { Session, Machine } from '@/sync/storageTypes';
import { Ionicons } from '@expo/vector-icons';
import { getSessionName, useSessionStatus, getSessionAvatarId, formatPathRelativeToHome } from '@/utils/sessionUtils';
import { getSessionName, useSessionStatus, getSessionAvatarId, formatPathRelativeToHome, getMachineDisplayName } from '@/utils/sessionUtils';
import { Avatar } from './Avatar';
import { Typography } from '@/constants/Typography';
import { StatusDot } from './StatusDot';
Expand Down Expand Up @@ -94,7 +94,13 @@ const stylesheet = StyleSheet.create((theme, runtime) => ({
sessionTitleRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 2,
},
sessionSubtitle: {
fontSize: 12,
color: theme.colors.textSecondary,
marginBottom: 4,
...Typography.default(),
},
sessionTitle: {
fontSize: 15,
Expand Down Expand Up @@ -339,6 +345,7 @@ const CompactSessionRow = React.memo(({ session, selected, showBorder }: { sessi
const styles = stylesheet;
const sessionStatus = useSessionStatus(session);
const sessionName = getSessionName(session);
const hostname = getMachineDisplayName(session);
const navigateToSession = useNavigateToSession();
const isTablet = useIsTablet();
const swipeableRef = React.useRef<Swipeable | null>(null);
Expand Down Expand Up @@ -406,6 +413,13 @@ const CompactSessionRow = React.memo(({ session, selected, showBorder }: { sessi
</Text>
</View>

{/* Hostname subtitle */}
{hostname && (
<Text style={styles.sessionSubtitle} numberOfLines={1}>
{hostname}
</Text>
)}

{/* Status line with dot */}
<View style={styles.statusRow}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
Expand Down
193 changes: 193 additions & 0 deletions packages/happy-app/sources/utils/machineDisplay.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { describe, it, expect } from 'vitest';
import { getMachineDisplayName, type MinimalSession, type MinimalMachine } from './machineDisplay';

describe('machineDisplay', () => {
describe('getMachineDisplayName', () => {
const baseSession: MinimalSession = {
metadata: {
host: 'lute.example.com',
machineId: 'machine-1'
}
};

const baseMachine: MinimalMachine = {
metadata: {
host: 'lute.example.com'
}
};

describe('priority 1: Machine displayName', () => {
it('should use machine displayName when available', () => {
const machine: MinimalMachine = {
metadata: {
host: 'lute.example.com',
displayName: 'My MacBook Pro'
}
};
expect(getMachineDisplayName(baseSession, machine)).toBe('My MacBook Pro');
});

it('should prioritize displayName over hostname', () => {
const machine: MinimalMachine = {
metadata: {
host: 'lute.example.com',
displayName: 'Dev Server'
}
};
expect(getMachineDisplayName(baseSession, machine)).toBe('Dev Server');
});
});

describe('priority 2: Machine short hostname', () => {
it('should use short hostname from machine metadata', () => {
const machine: MinimalMachine = {
metadata: {
host: 'lute.example.com'
}
};
expect(getMachineDisplayName(baseSession, machine)).toBe('lute');
});

it('should return full hostname if no dots', () => {
const machine: MinimalMachine = {
metadata: {
host: 'localhost'
}
};
expect(getMachineDisplayName(baseSession, machine)).toBe('localhost');
});

it('should handle hostname with multiple dots', () => {
const machine: MinimalMachine = {
metadata: {
host: 'web01.prod.example.com'
}
};
expect(getMachineDisplayName(baseSession, machine)).toBe('web01');
});
});

describe('priority 3: Session metadata host fallback', () => {
it('should fall back to session metadata host when machine is null', () => {
expect(getMachineDisplayName(baseSession, null)).toBe('lute');
});

it('should fall back to session metadata host when machine is undefined', () => {
expect(getMachineDisplayName(baseSession, undefined)).toBe('lute');
});

it('should fall back when machine metadata is null', () => {
const machine: MinimalMachine = {
metadata: null
};
expect(getMachineDisplayName(baseSession, machine)).toBe('lute');
});

it('should extract short hostname from FQDN in session', () => {
const session: MinimalSession = {
metadata: {
host: 'server.internal.company.com',
machineId: 'machine-1'
}
};
expect(getMachineDisplayName(session, null)).toBe('server');
});
});

describe('edge cases', () => {
it('should return undefined when no hostname available', () => {
const session: MinimalSession = {
metadata: null
};
expect(getMachineDisplayName(session, null)).toBeUndefined();
});

it('should return undefined when session metadata has empty host', () => {
const session: MinimalSession = {
metadata: {
host: '',
machineId: 'machine-1'
}
};
expect(getMachineDisplayName(session, null)).toBeUndefined();
});

it('should handle empty displayName by falling back', () => {
const machine: MinimalMachine = {
metadata: {
host: 'lute.example.com',
displayName: ''
}
};
// Empty string is falsy, should fall back to hostname
expect(getMachineDisplayName(baseSession, machine)).toBe('lute');
});
});

describe('common scenarios', () => {
it('should work for localhost', () => {
const session: MinimalSession = {
metadata: {
host: 'localhost'
}
};
expect(getMachineDisplayName(session, null)).toBe('localhost');
});

it('should work for remote server with FQDN', () => {
const session: MinimalSession = {
metadata: {
host: 'prod-server-01.us-west-2.company.com'
}
};
expect(getMachineDisplayName(session, null)).toBe('prod-server-01');
});

it('should work with custom machine names', () => {
const machine: MinimalMachine = {
metadata: {
host: 'macbook.local',
displayName: "Jon's MacBook Pro"
}
};
expect(getMachineDisplayName(baseSession, machine)).toBe("Jon's MacBook Pro");
});
});

describe('integration examples', () => {
it('should combine with path for complete subtitle', () => {
const session: MinimalSession = {
metadata: {
host: 'lute.example.com',
machineId: 'machine-1'
}
};
const path = '~/projects/myapp';
const hostname = getMachineDisplayName(session, null);

expect(hostname).toBe('lute');
expect(`${hostname}:${path}`).toBe('lute:~/projects/myapp');
});

it('should work with custom display name in subtitle', () => {
const session: MinimalSession = {
metadata: {
host: 'macbook.local',
machineId: 'machine-1'
}
};
const machine: MinimalMachine = {
metadata: {
host: 'macbook.local',
displayName: 'My MacBook'
}
};
const path = '~/Code/happy';
const hostname = getMachineDisplayName(session, machine);

expect(hostname).toBe('My MacBook');
expect(`${hostname}:${path}`).toBe('My MacBook:~/Code/happy');
});
});
});
});
47 changes: 47 additions & 0 deletions packages/happy-app/sources/utils/machineDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Pure utility functions for machine/hostname display logic.
* No React or React Native dependencies - testable in node environment.
*/

export type MinimalSession = {
metadata: {
host: string;
machineId?: string;
} | null;
};

export type MinimalMachine = {
metadata: {
host: string;
displayName?: string;
} | null;
};

/**
* Gets the display name for a machine/host.
* Prioritizes: Machine.displayName > short hostname > session.metadata.host
* @param session - The session containing metadata
* @param machine - Optional machine object
* @returns Display name for the machine, or undefined if not available
*/
export function getMachineDisplayName(
session: MinimalSession,
machine?: MinimalMachine | null
): string | undefined {
// Priority 1: Use machine's custom display name if available
if (machine?.metadata?.displayName) {
return machine.metadata.displayName;
}

// Priority 2: Use machine's short hostname
if (machine?.metadata?.host) {
return machine.metadata.host.split('.')[0];
}

// Priority 3: Fall back to session metadata host (short format)
if (session.metadata?.host) {
return session.metadata.host.split('.')[0];
}

return undefined;
}
Loading