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
7 changes: 7 additions & 0 deletions src/apps/mobile/harmonyos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ Device QR codes identify a target; the authenticated account directory authorize
access. Old room records and local conversation data remain on disk during an
upgrade, but do not automatically reconnect or start a local runtime.

GitHub usernames and avatars are presentation metadata loaded from GitHub's public
user-by-ID API without forwarding account credentials. The phone caches them for
24 hours in its encrypted account store, scoped to the authenticated GitHub ID.
Existing signed-in installs are enriched on startup. Offline or rate-limited
profile requests retain the session and cached display; device authorization
continues to use the immutable ID issued by Relay.

## Project Layout

- `AppScope/`: application metadata and shared resources.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const EN_US_MESSAGES: [string, string][] = [
['remote.settings.deviceUnavailable', 'The target desktop is unavailable. Refresh the device list and retry.'],
['remote.settings.deviceName', 'Device'],
['remote.settings.deviceManagementBody', 'This device stores account credentials encrypted with HUKS. Signing out clears them immediately.'],
['remote.settings.githubId', 'GitHub ID: {0}'],
['remote.settings.githubAccount', 'GitHub account'],
['remote.settings.userId', 'User ID'],
['remote.settings.deviceId', 'This device ID'],
['remote.settings.connectedDesktop', 'Current desktop'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const ZH_CN_MESSAGES: [string, string][] = [
['remote.settings.deviceUnavailable', '目标桌面暂时不可用,请刷新设备列表后重试。'],
['remote.settings.deviceName', '设备'],
['remote.settings.deviceManagementBody', '此设备的账号凭据已使用 HUKS 加密保存。退出账号后会立即清理。'],
['remote.settings.githubId', 'GitHub ID: {0}'],
['remote.settings.githubAccount', 'GitHub 账号'],
['remote.settings.userId', '用户 ID'],
['remote.settings.deviceId', '本机设备 ID'],
['remote.settings.connectedDesktop', '当前桌面端'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export interface RemoteHomePresentationActions {

export interface SidebarPresentationActions {
readonly close: () => void;
readonly newChat: () => void;
readonly newChat: (agentType?: string) => void;
readonly enterCode: () => void;
/** Opens the QR scanner directly from sidebar device actions. */
readonly scanDesktop: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SheetCloseHeader } from './SheetCloseHeader';
@ComponentV2
export struct AccountProfilePanel {
@Param accountUsername: string = '';
@Param accountAvatarUrl: string = '';
@Param accountUserId: string = '';
@Param fallbackUserId: string = '';
@Param deviceId: string = '';
Expand Down Expand Up @@ -73,7 +74,7 @@ export struct AccountProfilePanel {
private CompactIdentityCard() {
this.EmbeddedSectionTitle(RemoteI18n.t('settings.account.section'))
Row({ space: 14 }) {
DefaultAccountAvatar({ avatarSize: 46 })
DefaultAccountAvatar({ avatarSize: 46, avatarUrl: this.accountAvatarUrl })
Column({ space: 4 }) {
Text(this.isAccountAuthenticated() ? RemoteI18n.t('settings.account.current') :
RemoteI18n.t('settings.account.currentIdentity'))
Expand All @@ -83,7 +84,7 @@ export struct AccountProfilePanel {
.width('100%')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.shortProfileIdentifier())
Text(this.isAccountAuthenticated() ? this.profileName() : this.shortProfileIdentifier())
.fontSize(MobileDesignTypography.bodySmall.size)
.fontColor(MUTED)
.width('100%')
Expand Down Expand Up @@ -141,7 +142,7 @@ export struct AccountProfilePanel {
@Builder
private ProfileHero() {
Column({ space: 10 }) {
DefaultAccountAvatar({ avatarSize: 70 })
DefaultAccountAvatar({ avatarSize: 70, avatarUrl: this.accountAvatarUrl })
Text(this.profileName())
.fontSize(MobileDesignTypography.headlineLarge.size)
.fontWeight(FontWeight.Bold)
Expand All @@ -150,7 +151,7 @@ export struct AccountProfilePanel {
.textAlign(TextAlign.Center)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.profileIdentifier())
Text(this.isAccountAuthenticated() ? RemoteI18n.f('remote.settings.githubId', this.accountUserId) : this.profileIdentifier())
.fontSize(MobileDesignTypography.bodyMedium.size)
.fontColor(MUTED)
.width('88%')
Expand Down Expand Up @@ -445,7 +446,8 @@ export struct AccountProfilePanel {
}

private profileName(): string {
return this.accountUsername || RemoteI18n.t('remote.settings.openbitfunUser');
return this.accountUsername && this.accountUsername !== this.accountUserId ? `@${this.accountUsername}` :
RemoteI18n.t('remote.settings.githubAccount');
}

private profileIdentifier(): string {
Expand All @@ -472,7 +474,7 @@ export struct AccountProfilePanel {
if (!this.isAccountAuthenticated()) {
return RemoteI18n.t('remote.settings.accountNotSignedInBody');
}
return RemoteI18n.f('remote.settings.accountSignedInBody', this.accountUsername || this.accountUserId);
return RemoteI18n.f('remote.settings.accountSignedInBody', this.profileName());
}

private isAccountAuthenticated(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export struct AppSidebarSurface {
build() {
AppSidebar({
connectionState: this.remotePageState.connectionState,
isBusy: this.remotePageState.isBusy,
workspaceKind: this.remotePageState.workspaceKind,
workspacePath: this.remotePageState.workspacePath,
controlTargetDeviceId: this.remotePageState.controlTargetDeviceId,
supportsHarnessProfiles: this.remotePageState.supportsHostCapability(REMOTE_CAPABILITY_HARNESS_PROFILES_V1),
accountUserId: this.remotePageState.accountUserId,
controlTargetType: this.remotePageState.controlTargetType,
showViewSettingsButton: false,
Expand Down Expand Up @@ -123,6 +128,7 @@ export struct AppSettingsSurface {
desktopId: this.remotePageState.desktopId,
userId: this.remotePageState.userId,
accountUsername: this.remotePageState.accountUsername,
accountAvatarUrl: this.remotePageState.accountAvatarUrl,
accountUserId: this.remotePageState.accountUserId,
deviceId: this.deviceId,
controlTargetType: this.remotePageState.controlTargetType,
Expand All @@ -148,6 +154,7 @@ export struct AppSettingsSurface {
SettingsSheet({
sheetState: this.shellState.settingsSheet,
accountUsername: this.remotePageState.accountUsername,
accountAvatarUrl: this.remotePageState.accountAvatarUrl,
authenticatedUserId: this.remotePageState.accountUserId,
fallbackUserId: this.remotePageState.userId,
desktopName: this.remotePageState.desktopName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HarnessProfileMenu } from './HarnessProfileMenu';
import { MobileDesignGeometry, MobileDesignTypography } from '../../generated/MobileDesignTokens';
import { RemoteI18n } from '../../i18n/RemoteI18n';
import { CARD, INK, LINE, PAGE_BG, PAGE_BG_FADE, SHADOW_FAINT, SHADOW_SUBTLE, SOFT, SUBTLE, TRANSPARENT } from './Theme';
Expand All @@ -9,13 +10,19 @@ import { SidebarConnectionActionPolicy } from '../policy/SidebarConnectionAction
@ComponentV2
export struct AppSidebar {
@Param connectionState: string = 'idle';
@Param isBusy: boolean = false;
@Param workspaceKind: string = 'normal';
@Param workspacePath: string = '';
@Param controlTargetDeviceId: string = '';
@Param supportsHarnessProfiles: boolean = false;
@Local showCreateMenu: boolean = false;
@Param accountUserId: string = '';
@Param controlTargetType: string = 'none';
@Param showCollapseButton: boolean = false;
@Param showViewSettingsButton: boolean = false;
@Param showWorkspaceSection: boolean = false;
@Event onClose: () => void = () => {};
@Event onNewChat: () => void = () => {};
@Event onNewChat: (agentType: string) => void = (_agentType: string) => {};
@Event onEnterCode: () => void = () => {};
@Event onScanDesktop: () => void = () => {};
@Event onCollapse: () => void = () => {};
Expand Down Expand Up @@ -162,7 +169,7 @@ export struct AppSidebar {
.fontSize(MobileDesignTypography.bodyMedium.size)
.fontColor(INK)
.placeholderColor(SUBTLE)
.padding({ left: 14, right: 14 })
.padding(0)
.backgroundColor(SOFT)
.borderRadius(8)
.margin({ top: 12 })
Expand Down Expand Up @@ -195,7 +202,7 @@ export struct AppSidebar {
.height(50)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.onNewChat();
this.onEnterCode();
})

if (this.showCollapseButton) {
Expand All @@ -215,8 +222,8 @@ export struct AppSidebar {
Row() {
Button() {
Row({ space: 8 }) {
SidebarGlyph({ kind: 'remote' })
Text(RemoteI18n.t('sidebar.devices'))
SidebarGlyph({ kind: 'edit' })
Text(RemoteI18n.t('sidebar.newChat'))
.fontSize(MobileDesignTypography.titleMedium.size)
.fontWeight(FontWeight.Medium)
.fontColor(INK)
Expand All @@ -229,10 +236,23 @@ export struct AppSidebar {
.border({ width: 0.5, color: LINE })
.borderRadius(22)
.shadow({ radius: 24, color: SHADOW_SUBTLE, offsetY: 7 })
.enabled(SidebarConnectionActionPolicy.canCreateSession(this.connectionState, this.isBusy))
.opacity(SidebarConnectionActionPolicy.canCreateSession(this.connectionState, this.isBusy) ? 1 : 0.45)
.accessibilityText(RemoteI18n.t('sidebar.newChat'))
.stateEffect(true)
.onClick(() => {
this.onNewChat();
.bindPopup(this.showCreateMenu, {
builder: () => { this.CreateModeMenu() },
placement: Placement.Top,
popupColor: TRANSPARENT,
enableArrow: false,
autoCancel: true,
mask: false,
targetSpace: 6,
onStateChange: (event) => {
if (!event.isVisible) this.showCreateMenu = false;
}
})
.onClick(() => { this.requestChat(); })

Blank()

Expand Down Expand Up @@ -301,6 +321,33 @@ export struct AppSidebar {
.zIndex(2)
}

@Monitor('controlTargetDeviceId', 'workspacePath', 'workspaceKind', 'connectionState', 'isBusy', 'supportsHarnessProfiles')
private closeStaleCreateMenu(): void {
this.showCreateMenu = false;
}

private requestChat(): void {
if (!SidebarConnectionActionPolicy.canCreateSession(this.connectionState, this.isBusy)) return;
const action = SidebarConnectionActionPolicy.chatAction(this.workspaceKind, this.supportsHarnessProfiles);
if (action === 'choose-harness') {
this.showCreateMenu = !this.showCreateMenu;
return;
}
this.onNewChat(action === 'assistant' ? 'claw' : 'code');
}

@Builder
private CreateModeMenu() {
HarnessProfileMenu({
onSelect: (agentType: string) => {
this.showCreateMenu = false;
if (SidebarConnectionActionPolicy.canCreateSession(this.connectionState, this.isBusy)) {
this.onNewChat(agentType);
}
}
})
}

private shouldShowPrimaryNavigation(): boolean {
return SidebarConnectionActionPolicy.shouldShowPrimaryNavigation(
this.accountUserId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@ import { MUTED, SOFT } from './Theme';
@ComponentV2
export struct DefaultAccountAvatar {
@Param avatarSize: number = 34;
@Param avatarUrl: string = '';
@Local failedUrl: string = '';

build() {
Stack({ alignContent: Alignment.Center }) {
SymbolGlyph($r('sys.symbol.person'))
.fontSize(this.avatarSize * 0.52)
.fontColor([MUTED])
if (this.avatarUrl.length > 0 && this.failedUrl !== this.avatarUrl) {
Image(this.avatarUrl)
.width(this.avatarSize)
.height(this.avatarSize)
.objectFit(ImageFit.Cover)
.borderRadius(this.avatarSize / 2)
.onError(() => { this.failedUrl = this.avatarUrl; })
}
}
.width(this.avatarSize)
.height(this.avatarSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export struct RemoteControlSettingsSheet {
@Param desktopId: string = '';
@Param userId: string = '';
@Param accountUsername: string = '';
@Param accountAvatarUrl: string = '';
@Param accountUserId: string = '';
@Param deviceId: string = '';
@Param controlTargetType: string = 'none';
Expand Down Expand Up @@ -110,7 +111,7 @@ export struct RemoteControlSettingsSheet {
@Builder
private ProfileEntry() {
Row({ space: 12 }) {
DefaultAccountAvatar({ avatarSize: 34 })
DefaultAccountAvatar({ avatarSize: 34, avatarUrl: this.accountAvatarUrl })
Text(this.isAccountAuthenticated() ? RemoteI18n.t('remote.settings.profile') :
RemoteI18n.t('sidebar.signInOpenBitFunAccount'))
.fontSize(MobileDesignTypography.headlineSmall.size)
Expand Down Expand Up @@ -449,6 +450,7 @@ export struct RemoteControlSettingsSheet {
Scroll() {
AccountProfilePanel({
accountUsername: this.accountUsername,
accountAvatarUrl: this.accountAvatarUrl,
accountUserId: this.accountUserId,
fallbackUserId: this.userId,
deviceId: this.deviceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CloudAccountDevice } from '../../services/CloudAccountClient';
export struct SettingsSheet {
@Param sheetState: SettingsSheetState = new SettingsSheetState();
@Param accountUsername: string = '';
@Param accountAvatarUrl: string = '';
@Param authenticatedUserId: string = '';
@Param fallbackUserId: string = '';
@Param desktopName: string = '';
Expand Down Expand Up @@ -58,6 +59,7 @@ export struct SettingsSheet {
Column({ space: 0 }) {
AccountProfilePanel({
accountUsername: this.accountUsername,
accountAvatarUrl: this.accountAvatarUrl,
accountUserId: this.authenticatedUserId,
fallbackUserId: this.fallbackUserId,
deviceId: this.deviceId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
/** Presentation rules for account-independent sidebar entry and navigation. */
export type SidebarChatAction = 'assistant' | 'choose-harness' | 'code';

export class SidebarConnectionActionPolicy {
/** Match mobile-web: assistant chat, supported harness chooser, or legacy code chat. */
static chatAction(workspaceKind: string, supportsHarnessProfiles: boolean): SidebarChatAction {
if (workspaceKind === 'assistant') return 'assistant';
return supportsHarnessProfiles ? 'choose-harness' : 'code';
}

static canCreateSession(connectionState: string, isBusy: boolean): boolean {
return connectionState === 'connected' && !isBusy;
}

static shouldShowScan(connectionState: string): boolean {
return connectionState !== 'connected';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SidebarConnectionActionPolicy } from '../policy/SidebarConnectionActionPolicy';
import { accountDeviceLink } from '../../services/AccountDeviceLink';
import {
ChatMessage,
Expand All @@ -18,6 +19,7 @@ import { ConnectionStatusPresenter } from '../../services/ConnectionStatusPresen
import { ImagePickerService } from '../../services/ImagePickerService';
import { MobileIdentityStore } from '../../services/MobileIdentityStore';
import { CloudAccountClient, CloudAccountDevice } from '../../services/CloudAccountClient';
import { GitHubAccountProfileClient } from '../../services/GitHubAccountProfile';
import { CloudAccountSessionStore } from '../../services/CloudAccountSessionStore';
import { RemoteActivityLifecycleController } from '../../services/RemoteActivityLifecycleController';
import { RemoteChatCache } from '../../services/RemoteChatCache';
Expand Down Expand Up @@ -610,6 +612,7 @@ export abstract class AppRootRuntimeComposition {
{
client: new CloudAccountClient(),
sessionStore: new CloudAccountSessionStore(),
profileLoader: new GitHubAccountProfileClient(),
sessionManager: this.sessionManager,
remoteState: this.remotePageState,
hooks: {
Expand Down Expand Up @@ -785,7 +788,11 @@ export abstract class AppRootRuntimeComposition {
},
onSidebar: {
close: (): void => this.closeAppSidebar(),
newChat: (): void => { this.closeAppSidebar(); this.enterRemoteSurface(); },
newChat: async (agentType: string = 'code'): Promise<void> => {
if (!SidebarConnectionActionPolicy.canCreateSession(this.remotePageState.connectionState, this.remotePageState.isBusy)) return;
this.closeAppSidebar();
await this.conversationController.createRemoteSession(agentType);
},
enterCode: (): void => { this.closeAppSidebar(); this.enterCodeEntry(); },
scanDesktop: (): void => { this.closeAppSidebar(); this.openConnectSheet(CONNECT_INTENT_SCAN); },
addDesktop: (): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class RemotePageState {
@Trace userId: string = '';
@Trace requiresAccountAuth: boolean = false;
@Trace accountUsername: string = '';
@Trace accountAvatarUrl: string = '';
@Trace accountUserId: string = '';
@Trace authenticatedUserId: string = '';
@Trace hostCapabilities: string[] = [];
Expand Down Expand Up @@ -103,6 +104,10 @@ export class RemotePageState {
this.accountUsername = accountUsername;
}

setAccountAvatarUrl(avatarUrl: string): void {
this.accountAvatarUrl = avatarUrl;
}

setAccountUsername(accountUsername: string): void {
this.accountUsername = accountUsername;
}
Expand Down
Loading
Loading