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
24 changes: 24 additions & 0 deletions src/apps/mobile/harmonyos/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# HarmonyOS App Instructions

These rules apply to all changes under `src/apps/mobile/harmonyos`.

## Visual reference fidelity

- Before drawing a system glyph, text approximation, or new bitmap, search the existing HarmonyOS media resources and the approved desktop reference images. Reuse the established asset when one exists.
- Conversation header controls must use the approved `remote_ref_back` and `remote_ref_more` assets. Do not replace them with a system chevron or text such as `...` / bullet characters.
- Render monochrome reference assets in template mode and tint them with semantic theme colors such as `INK`. Never rely on the bitmap's original black or white pixels; the same control must remain legible in light and dark themes.
- Keep paired header controls on the same fixed touch-target size and optical alignment. A responsive layout may reposition a control, but must not silently change its icon geometry or visual weight.
- Keep `SymbolGlyph` geometry separate from its touch target. When a glyph is clickable or sits in a decorated control, wrap it in `Stack({ alignContent: Alignment.Center })` (or use a centered `Button`) and give the glyph its visual size; do not stretch the glyph itself to the full 32vp/40vp/44vp target, because font metrics can make the icon look off-center.

## Responsive interaction semantics

- Wide and compact layouts must keep the same interaction meaning. Responsive presentation may change spacing and available width, but it must not turn a lightweight anchored action menu into a bottom sheet by default.
- Conversation-header overflow actions open from the top-right trigger as an anchored popover on both compact and wide layouts. Use a bottom sheet only when the content is a genuinely large or multi-step mobile workflow and the design explicitly calls for it.
- Anchor popovers to their actual trigger with `bindPopup` or the equivalent platform API. Do not emulate the anchor with unrelated page-level absolute positioning.
- Preserve auto-dismiss, outside-tap handling, accessibility labels, and a short enter/exit transition for anchored menus.

## Theme and device verification

- Use existing semantic colors from `Theme.ets`; do not hard-code a light-only foreground or surface color.
- For changes to navigation controls, menus, or responsive presentation, verify compact and wide behavior, light and dark theme legibility, and capture a real-device screenshot before completion when a device is connected.
- Run the smallest matching HarmonyOS build/check plus `pnpm run theme:color-audit:all` for theme or color-related changes.
6 changes: 3 additions & 3 deletions src/apps/mobile/harmonyos/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# BitFun HarmonyOS

Native HarmonyOS phone client for BitFun. The application provides general
chat and remote control of BitFun desktop sessions.
Native HarmonyOS client for BitFun. The application provides general chat and
remote control of BitFun desktop sessions on phone and tablet devices.

## Project Layout

Expand Down Expand Up @@ -29,4 +29,4 @@ Signing configuration is intentionally not stored in the repository. Configure
a local signing identity in DevEco Studio when installing the app on a device.

The current project targets HarmonyOS `6.1.1(24)` and supports
`6.0.1(21)` or newer on phone devices.
`6.0.1(21)` or newer on phone and tablet devices.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { AbilityConstant, Configuration, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;
const TAG = 'BitFunRemote';

export default class EntryAbility extends UIAbility {
private mainWindow?: window.Window;

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
Expand All @@ -24,13 +26,9 @@ export default class EntryAbility extends UIAbility {
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onWindowStageCreate');
try {
const mainWindow = windowStage.getMainWindowSync();
this.mainWindow = mainWindow;
mainWindow.setWindowLayoutFullScreen(false);
mainWindow.setWindowSystemBarProperties({
statusBarColor: '#FAFAF8',
navigationBarColor: '#FAFAF8',
statusBarContentColor: '#171717',
navigationBarContentColor: '#171717'
});
this.updateSystemBars(this.context.config?.colorMode);
} catch (err) {
hilog.error(DOMAIN, TAG, 'Failed to configure window bars. Cause: %{public}s', JSON.stringify(err));
}
Expand All @@ -54,8 +52,25 @@ export default class EntryAbility extends UIAbility {
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onForeground');
}

onConfigurationUpdate(newConfig: Configuration): void {
this.updateSystemBars(newConfig.colorMode);
}

onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onBackground');
}

private updateSystemBars(colorMode?: ConfigurationConstant.ColorMode): void {
if (!this.mainWindow) {
return;
}
const dark = colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
this.mainWindow.setWindowSystemBarProperties({
statusBarColor: dark ? '#151514' : '#FDFDFB',
navigationBarColor: dark ? '#151514' : '#FDFDFB',
statusBarContentColor: dark ? '#F4F3EF' : '#171717',
navigationBarContentColor: dark ? '#F4F3EF' : '#171717'
});
}
}
59 changes: 57 additions & 2 deletions src/apps/mobile/harmonyos/entry/src/main/ets/i18n/RemoteI18n.ets
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const ZH_CN_MESSAGES: [string, string][] = [
['app.title', 'BitFun'],

['common.cancel', '取消'],
['common.back', '返回'],
['common.close', '关闭'],
['common.copy', '复制'],
['common.current', '当前'],
Expand Down Expand Up @@ -48,6 +49,7 @@ const ZH_CN_MESSAGES: [string, string][] = [
['generalChat.interrupted', '回复已中断,已保留收到的内容。'],
['generalChat.replyInterrupted', '回复中断'],
['generalChat.fileDownloadMock', '普通聊天暂不支持下载桌面端文件,请进入 Code 后处理本地文件。'],
['generalChat.filePreviewUnavailable', '普通聊天暂不支持预览桌面端文件,请进入 Code 后打开。'],
['generalChat.localRestoreFailed', '普通对话历史暂时无法恢复,你仍可新建对话。'],
['generalChat.modelNotConfigured', '请先在设置中配置普通对话模型。'],
['generalChat.imageNotSupported', '当前模型通道暂不支持图片,请先发送文字消息。'],
Expand All @@ -58,12 +60,22 @@ const ZH_CN_MESSAGES: [string, string][] = [
['generalChat.emptyResponse', '模型没有返回文字内容。'],
['generalChat.requestFailed', '模型请求失败(HTTP {0})。'],

['settings.modelService.section', '模型服务'],
['settings.modelService.section', '普通对话'],
['settings.title', '设置'],
['settings.about.section', '关于'],
['settings.about.product', '产品'],
['settings.about.version', '版本'],
['settings.modelService.title', '普通对话模型'],
['settings.modelService.title', '模型'],
['settings.modelService.manageTitle', '普通对话模型'],
['settings.modelService.localTitle', '本机自定义模型'],
['settings.modelService.currentModel', '当前使用'],
['settings.modelService.accountModels', '账号同步'],
['settings.modelService.accountModelSummary', '云端账号模型'],
['settings.modelService.localModel', '本机自定义'],
['settings.modelService.accountSource', '云端账号'],
['settings.modelService.localSource', '本机'],
['settings.modelService.syncedCount', '已同步 {0} 个'],
['settings.modelService.accountEmpty', '暂无可用的账号模型'],
['settings.modelService.configured', '已配置'],
['settings.modelService.notConfigured', '未配置'],
['settings.modelService.apiUrl', 'API URL'],
Expand Down Expand Up @@ -107,6 +119,31 @@ const ZH_CN_MESSAGES: [string, string][] = [
['sidebar.noSearchResult', '没有匹配的会话'],
['sidebar.signedOutNewChat', '新聊天'],
['sidebar.signInBitFunAccount', '登录BitFun账号'],
['sidebar.collapse', '收起侧边栏'],
['sidebar.restore', '展开侧边栏'],
['session.actions', '会话操作'],
['session.details', '会话详情'],
['session.viewDetails', '查看详情'],
['session.agentType', 'Agent 类型'],
['session.workspace', '工作区'],
['session.workspacePath', '工作区路径'],
['session.createdAt', '创建时间'],
['session.updatedAt', '更新时间'],
['session.messageCount', '消息数'],
['session.status', '状态'],
['viewSettings.title', '视图设置'],
['viewSettings.subtitle', '调整会话列表的分组和信息密度'],
['viewSettings.grouping', '分组方式'],
['viewSettings.filters', '筛选'],
['viewSettings.metadata', '显示信息'],
['viewSettings.workspace', '工作区'],
['viewSettings.agentType', '会话类型'],
['viewSettings.allWorkspaces', '全部工作区'],
['viewSettings.allAgentTypes', '全部类型'],
['viewSettings.allStatuses', '全部状态'],
['viewSettings.updated', '更新时间'],
['viewSettings.status', '运行状态'],
['common.unknown', '未知'],

['code.title', 'BitFun Code'],
['code.heroTitle', '本地开发助手'],
Expand All @@ -126,17 +163,22 @@ const ZH_CN_MESSAGES: [string, string][] = [
['remote.searchChats', '搜索聊天记录'],
['remote.emptyTitle', '还没有远程对话'],
['remote.emptyText', '新建聊天后,可以从手机继续处理桌面端任务。'],
['remote.pickSession', '选择一个会话'],
['remote.pickSessionText', '从侧边栏打开会话,或新建一个。'],
['remote.startSession', '新建会话'],
['remote.connectTitle', '连接桌面端'],
['remote.connectText', '扫描桌面端显示的二维码,开始远程处理任务。'],
['remote.actions', '远程设置'],
['remote.device', '连接的设备'],
['remote.newChat', '聊天'],
['remote.create.title', '新建任务'],
['remote.create.chat', '聊天'],
['remote.create.noDevice', '选择桌面设备'],
['remote.create.noOnlineDevice', '没有可用的在线桌面设备'],
['remote.create.placeholder', '告诉 BitFun 要做什么'],
['remote.create.deviceLoadFailed', '设备列表加载失败,请稍后重试。'],
['remote.create.workspaceLoadFailed', '工作区加载失败,请稍后重试。'],
['remote.create.deviceMismatch', '所选设备尚未连接,请重新选择设备后再试。'],
['remote.create.submitFailed', '无法创建会话,请检查桌面连接后重试。'],
['remote.workspace', '工作区'],
['remote.assistant', '助理'],
Expand Down Expand Up @@ -397,6 +439,19 @@ const ZH_CN_MESSAGES: [string, string][] = [
['chat.fileLink', '文件链接'],
['chat.reading', '读取中'],
['chat.download', '下载'],
['filePreview.title', '文件预览'],
['filePreview.loading', '正在读取文件'],
['filePreview.offline', '离线,仅显示已加载内容'],
['filePreview.fitImage', '适应窗口'],
['filePreview.actualImageSize', '原始大小'],
['filePreview.imageDecodeFailed', '图片无法解码,请重试或下载文件。'],
['filePreview.loadFailed', '无法打开文件'],
['filePreview.notFound', '文件不存在或已被移动'],
['filePreview.unavailable', '文件不存在或不在当前工作区'],
['filePreview.accessDenied', '无法访问工作区外的文件'],
['filePreview.tooLarge', '文件过大,无法在移动端预览'],
['filePreview.unsupported', '此文件暂不支持预览'],
['filePreview.truncated', '已显示前 {0},下载后可查看完整内容'],
['chat.pendingConfirmation', '待确认'],
['chat.cancelled', '已取消'],
['chat.jsonObjectRequired', '请输入 JSON 对象'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface CreateSessionOptions {
agentType: string;
title: string;
instruction: string;
modelId?: string;
}

export type RemotePermissionMode = 'ask' | 'auto' | 'full_access';
Expand Down Expand Up @@ -453,3 +454,12 @@ export interface ReadFileResult {
mimeType: string;
size: number;
}

export interface ReadFileChunkResult {
name: string;
contentBase64: string;
offset: number;
chunkSize: number;
totalSize: number;
mimeType: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ struct AppRoot {
this.runtime.aboutToDisappear();
}

onBackPress(): boolean {
return this.runtime.handleRootBack();
}

build() {
Stack() {
AppRootPresentation({
Expand All @@ -33,8 +37,8 @@ struct AppRoot {
remotePageState: this.runtime.remotePageState,
remoteCreateState: this.runtime.remoteCreateState,
generalPageState: this.runtime.generalChatPageState,
filePreviewState: this.runtime.filePreviewState,
deviceId: this.runtime.remoteConnectionViewModel.getDeviceId(),
currentRoute: this.runtime.currentRoute(),
actions: this.runtime.presentationActions
})
}
Expand Down
Loading