Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/mobile-web/src/components/CompactSettingsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface CompactSettingsSheetProps {
onSelectDevice: (device: SettingsDevice) => void;
onToggleTheme: () => void;
open: boolean;
renderDeviceIcon: (name: string) => React.ReactNode;
renderDeviceIcon: (device: SettingsDevice) => React.ReactNode;
selectedDeviceId: string | null;
}

Expand Down Expand Up @@ -140,7 +140,7 @@ export default function CompactSettingsSheet({
disabled={!device.online || !controllable}
key={device.device_id}
label={deviceDisplayName(device)}
leading={<span className="harmony-sidebar__settings-device-icon">{renderDeviceIcon(deviceDisplayName(device))}</span>}
leading={<span className="harmony-sidebar__settings-device-icon">{renderDeviceIcon(device)}</span>}
onClick={() => onSelectDevice(device)}
selected={current}
supportingText={!controllable
Expand Down
53 changes: 53 additions & 0 deletions src/mobile-web/src/components/DeviceSystemMark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Monitor, Smartphone } from 'lucide-react';
import {
DEVICE_SYSTEM_MARKS,
deviceSystemKeyFromOs,
type DeviceSystemKey,
} from '../../../shared/device-system/deviceSystemMarks';

interface DeviceSystemMarkProps {
/** Kind the device reported: a headless host draws the server silhouette. */
deviceKind?: string | null;
/** System the device reported. Absent system keeps a neutral mark. */
os?: string | null;
size: number;
}

/**
* The mark in front of a device name: the system that device reported, the server
* silhouette for a headless host, and the shape that says what a device is when
* it reports no system this client can draw.
*
* The marks themselves are shared with the desktop shell, so one device is drawn
* the same way on either surface. Size and colour are the caller's: mobile web
* sets its own icon scale, and every mark takes the colour of the row it sits in.
*/
export function DeviceSystemMark({ deviceKind, os, size }: DeviceSystemMarkProps) {
const kind = deviceKind?.trim().toLowerCase();
// A phone is not a system and a headless host has no window to draw, whatever
// either of them runs.
if (kind === 'mobile') {
return <Smartphone width={size} height={size} stroke="currentColor" aria-hidden="true" />;
}
const key: DeviceSystemKey | null = kind === 'cli' ? 'server' : deviceSystemKeyFromOs(os);
if (!key) {
return <Monitor width={size} height={size} stroke="currentColor" aria-hidden="true" />;
}
const mark = DEVICE_SYSTEM_MARKS[key];
return (
<svg
aria-hidden="true"
data-system={key}
focusable="false"
height={size}
viewBox={mark.viewBox}
width={size}
xmlns="http://www.w3.org/2000/svg"
style={mark.opticalShift === 0
? undefined
: { fontSize: `${size}px`, translate: `0 ${mark.opticalShift}em` }}
>
<path fill="currentColor" d={mark.path} />
</svg>
);
}
28 changes: 19 additions & 9 deletions src/mobile-web/src/pages/DevicesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { InvalidationSync } from '../../../shared/relay-transport/InvalidationSync';
import type { RelayFailureAction } from '../../../shared/relay-transport/RelayFailure';
import { DeviceSystemMark } from '../components/DeviceSystemMark';
import { deviceFailurePresentation } from '../services/deviceFailureCopy';
import {
ChevronLeft as LucideChevronLeft,
Monitor as LucideMonitor,
Pencil as LucidePencil,
RefreshCw as LucideRefreshCw,
UserRoundSearch as LucideUserRoundSearch,
} from 'lucide-react';
Expand Down Expand Up @@ -59,10 +61,6 @@ const RefreshIcon = () => (
<LucideRefreshCw width="16" height="16" stroke="currentColor" aria-hidden="true" />
);

const DeviceIcon = () => (
<LucideMonitor width="20" height="20" stroke="currentColor" aria-hidden="true" />
);

const NoIdentityIcon = () => (
<LucideUserRoundSearch width="40" height="40" stroke="currentColor" aria-hidden="true" />
);
Expand Down Expand Up @@ -304,7 +302,7 @@ const DevicesPage: React.FC<Props> = ({ client, onBack, onDeviceSelected = onBac
</div>
</div>
) : (
<>
<div className="devices-page__device-line">
<MobileListRow
appearance="surface"
className={[
Expand All @@ -315,7 +313,11 @@ const DevicesPage: React.FC<Props> = ({ client, onBack, onDeviceSelected = onBac
].filter(Boolean).join(' ')}
disabled={!clickable}
onClick={() => clickable && selectDevice(d)}
leading={<span className="devices-page__device-icon"><DeviceIcon /></span>}
leading={(
<span className="devices-page__device-icon">
<DeviceSystemMark deviceKind={d.device_kind} os={d.device_os} size={20} />
</span>
)}
label={(
<span className="devices-page__device-name-row">
<span className="devices-page__device-name">
Expand Down Expand Up @@ -348,10 +350,18 @@ const DevicesPage: React.FC<Props> = ({ client, onBack, onDeviceSelected = onBac

selected={isCurrent}
/>
<div className="devices-page__alias-actions">
<MobileButton size="sm" disabled={!aliasSupported} onClick={() => { setEditingId(d.device_id); setAliasDraft(d.device_alias ?? ''); }}>{t('devices.editAlias')}</MobileButton>
{/* Renaming is an action on the row, so it sits on the row's own line
instead of a block under it: one line per device at every width. */}
<MobileIconButton
appearance="plain"
className="devices-page__device-edit"
disabled={!aliasSupported}
icon={<LucidePencil width="18" height="18" stroke="currentColor" aria-hidden="true" />}
aria-label={t('devices.editAlias')}
title={t('devices.editAlias')}
onClick={() => { setEditingId(d.device_id); setAliasDraft(d.device_alias ?? ''); }}
/>
</div>
</>
)}
</div>
);
Expand Down
24 changes: 3 additions & 21 deletions src/mobile-web/src/pages/SessionListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
Ellipsis as LucideEllipsis,
Folder as LucideFolder,
FolderOpen as LucideFolderOpen,
Laptop as LucideLaptop,
LoaderCircle as LucideLoaderCircle,
LogOut as LucideLogOut,
MessageSquare as LucideMessageSquare,
Expand All @@ -17,7 +16,6 @@ import {
Plus as LucidePlus,
RefreshCw as LucideRefreshCw,
Search as LucideSearch,
Server as LucideServer,
Settings as LucideSettings,
Sun as LucideSun,
Terminal as LucideTerminal,
Expand All @@ -28,6 +26,7 @@ import {
} from 'lucide-react';
import { useGitHubAccountProfile } from '../hooks/useGitHubAccountProfile';
import AccountAvatar from '../components/AccountAvatar';
import { DeviceSystemMark } from '../components/DeviceSystemMark';
import React, { useEffect, useLayoutEffect, useRef, useCallback, useMemo, useState } from 'react';
import {
MobileButton,
Expand Down Expand Up @@ -252,23 +251,6 @@ function SessionTypeIcon({ agentType }: { agentType: string }) {
);
}

function CompactDeviceIcon({ name }: { name: string }) {
const normalized = name.toLocaleLowerCase();
if (/(macbook|laptop|notebook)/.test(normalized)) {
return (
<LucideLaptop width="22" height="22" stroke="currentColor" aria-hidden="true" />
);
}
if (/(server|ecs|cloud|host)/.test(normalized)) {
return (
<LucideServer width="22" height="22" stroke="currentColor" aria-hidden="true" />
);
}
return (
<LucideMonitor width="22" height="22" stroke="currentColor" aria-hidden="true" />
);
}

/* Mode Selection Icons */
const ProModeIcon = () => (
<LucideTerminal width="32" height="32" stroke="currentColor" aria-hidden="true" />
Expand Down Expand Up @@ -1620,7 +1602,7 @@ const SessionListPage: React.FC<SessionListPageProps> = ({
onClick={() => void handleSelectCompactDevice(device)}
>
<span className="harmony-sidebar__device-icon" aria-hidden="true">
<CompactDeviceIcon name={deviceDisplayName(device)}/>
<DeviceSystemMark deviceKind={device.device_kind} os={device.device_os} size={22} />
</span>
<span className="harmony-sidebar__row-label">{deviceDisplayName(device)}</span>
{isSwitching
Expand Down Expand Up @@ -1768,7 +1750,7 @@ const SessionListPage: React.FC<SessionListPageProps> = ({
onSelectDevice={(device) => void handleSelectCompactDevice(device)}
onToggleTheme={toggleTheme}
open={compactSettingsOpen}
renderDeviceIcon={(name) => <CompactDeviceIcon name={name} />}
renderDeviceIcon={(device) => <DeviceSystemMark deviceKind={device.device_kind} os={device.device_os} size={22} />}
selectedDeviceId={compactSelectedDeviceId}
/>

Expand Down
6 changes: 6 additions & 0 deletions src/mobile-web/src/services/RelayHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ type RelayRequestOptions = { retryable?: boolean; timeoutMs?: number };
export interface RelayDeviceInfo {
device_id: string;
device_name: string;
/**
* Kind the device reported to the Relay: `desktop`, `cli`, `mobile` or
* `watch`. Absent on an older Relay or a client that never reported one, so
* every reader has to keep a neutral answer for "unknown".
*/
device_kind?: string | null;
device_alias?: string | null;
device_model?: string | null;
device_os?: string | null;
Expand Down
37 changes: 32 additions & 5 deletions src/mobile-web/src/styles/components/devices.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,29 @@
background: var(--openbitfun-color-surface-panel);
}

.devices-page__device-line {
display: flex;
align-items: center;

& + & {
border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle);
}
}

.devices-page__device {
min-height: 72px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}

& + & { border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); }
/* The row is the selection target; renaming is a second target beside it, never
inside it, because a button cannot hold another button. */
.devices-page__device-edit {
flex-shrink: 0;
margin-right: var(--size-gap-1);
color: var(--openbitfun-color-content-muted);
}

.devices-page__device-icon {
Expand All @@ -84,10 +99,24 @@
.devices-page__device-name-row {
display: flex;
align-items: center;
gap: var(--size-gap-2);
flex-wrap: wrap;
gap: var(--size-gap-1) var(--size-gap-2);
min-width: 0;
}

/* The model and system line is the secondary half of the row. On a wide row it
sits beside the name; when it does not fit it moves to its own line rather
than eating the name, which is the device's identity. */
.devices-page__device-name-row > small {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--openbitfun-color-content-muted);
font-size: var(--openbitfun-type-body-xs-font-size);
}

.devices-page__device-name {
font-size: var(--openbitfun-type-body-lg-font-size);
font-weight: var(--openbitfun-type-label-lg-font-weight);
Expand Down Expand Up @@ -152,13 +181,11 @@
box-shadow: var(--openbitfun-shadow-xs);
}

/* Save and cancel under the alias editor, which owns its own spacing. */
.devices-page__alias-actions {
display: flex;
justify-content: flex-end;
gap: var(--size-gap-2);
padding: var(--size-gap-2) var(--size-gap-4) var(--size-gap-3);

.devices-page__alias-editor & { padding: 0; }
}

.devices-page__device-spinner {
Expand Down
Loading
Loading