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
20 changes: 19 additions & 1 deletion scripts/check-harmonyos-architecture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const allPages = filesUnder(pagesRoot);
const services = filesUnder(path.join(etsRoot, 'services'));
const components = allPages.filter((file) => file.includes(`${path.sep}pages${path.sep}components${path.sep}`));
const viewmodels = allPages.filter((file) => file.includes(`${path.sep}pages${path.sep}viewmodel${path.sep}`));
const stateFiles = allPages.filter((file) => file.includes(`${path.sep}pages${path.sep}state${path.sep}`));

const serviceToPages = services
.filter((file) => imports(file).some((spec) => spec === 'pages' || spec.startsWith('pages/')))
Expand All @@ -54,6 +55,9 @@ const componentToViewmodel = components
const viewmodelToComponents = viewmodels
.filter((file) => imports(file).some((spec) => spec === 'pages/components' || spec.startsWith('pages/components/')))
.map(relative);
const stateToComponents = stateFiles
.filter((file) => imports(file).some((spec) => spec === 'pages/components' || spec.startsWith('pages/components/')))
.map(relative);
const v1Components = allPages
.filter((file) => /^\s*@Component\s*$/m.test(fs.readFileSync(file, 'utf8')))
.map(relative);
Expand Down Expand Up @@ -98,10 +102,16 @@ const requiredPresentationFiles = [
'components/AppRootOverlaySurfaces.ets',
'components/ChatMessageChrome.ets',
'components/ConnectManualPairingOverlay.ets',
'components/ConversationHeader.ets',
'components/ConversationRouteSurface.ets',
'components/ToolInteractionPanels.ets',
'components/WideConversationHost.ets',
'components/remote/RemoteSurfaceHost.ets'
'components/remote/RemoteSurfaceHost.ets',
'policy/ConversationHeaderPolicy.ets',
'viewmodel/ConversationRuntime.ets',
'viewmodel/RemoteTranscriptController.ets',
'viewmodel/RemoteCreateFlowController.ets',
'viewmodel/VisibleConversationController.ets'
];
const missingPresentationFiles = requiredPresentationFiles
.filter((file) => !fs.existsSync(path.join(pagesRoot, file)));
Expand Down Expand Up @@ -268,6 +278,7 @@ const expected = {
serviceToPages: [],
componentToViewmodel: [],
viewmodelToComponents: [],
stateToComponents: [],
v1Components: [],
positionalActionConstructors: [],
duplicatedConversationTraceFields: [],
Expand All @@ -292,6 +303,7 @@ const actual = {
serviceToPages,
componentToViewmodel,
viewmodelToComponents,
stateToComponents,
v1Components,
positionalActionConstructors,
duplicatedConversationTraceFields,
Expand Down Expand Up @@ -324,6 +336,12 @@ if (appRootPresentationLines > 500) {
failed = true;
console.error(`AppRootPresentation line budget exceeded: expected <=500, actual=${appRootPresentationLines}`);
}
const conversationControllerFile = path.join(pagesRoot, 'viewmodel/ConversationController.ets');
const conversationControllerLines = fs.readFileSync(conversationControllerFile, 'utf8').split(/\r?\n/).length - 1;
if (conversationControllerLines > 400) {
failed = true;
console.error(`ConversationController line budget exceeded: expected <=400, actual=${conversationControllerLines}`);
}
for (const [file, budget] of componentLineBudgets) {
const source = fs.readFileSync(path.join(pagesRoot, file), 'utf8');
const lineCount = source.split(/\r?\n/).length - 1;
Expand Down
14 changes: 9 additions & 5 deletions src/apps/mobile/harmonyos/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,26 @@ the module. Keep the official responsibilities explicit:
intents/events rather than calling services directly.
- ViewModels bridge services and views by owning feature state, projecting data,
and handling intents. ViewModels must not import components.
- Shared conversation presentation DTOs (`ChatSurface`, `ChatComposerCapabilities`,
`ConversationUiModels`) live in `pages/state/`. State must not import
`pages/components`.

The following constraints are enforced incrementally by
`pnpm run harmony:architecture` (the runtime behavior checks remain in
`entry/src/test/ArchitectureUnit.test.ets`):

1. `services/**` must not import `../pages/`.
2. `pages/components/**` must not import `pages/viewmodel/`; imports of
2. `pages/state/**` must not import `pages/components/`.
3. `pages/components/**` must not import `pages/viewmodel/`; imports of
`pages/state/` and `pages/policy/` are allowed for observable state and pure
policies.
3. The page dependency graph must remain acyclic; ViewModels must not depend on
4. The page dependency graph must remain acyclic; ViewModels must not depend on
components.
4. Actions and Hooks use typed interfaces with object literals. Do not add
5. Actions and Hooks use typed interfaces with object literals. Do not add
position-dependent callback constructors.
5. New components use `@ComponentV2`; do not add V1 `@Component`, `@State`,
6. New components use `@ComponentV2`; do not add V1 `@Component`, `@State`,
`@Prop`, `@Link`, or `@Watch` declarations. `@BuilderParam` remains supported.
6. General Chat and Remote Chat shared observable fields belong to
7. General Chat and Remote Chat shared observable fields belong to
`pages/state/ConversationCoreState.ets`. Page-specific state objects compose
that core and must not redeclare the shared `@Trace` fields.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ Scope: `src/apps/mobile/harmonyos`,主要涉及双屏/三屏布局、会话来

### 已实现

- 展开态只要存在可用纵向折痕(双折叠一道、三折叠两道),且当前视口放得下 master + detail,就进入 master-detail;合盖仍强制单屏。无折痕时只有平板走宽屏,普通手机即使超宽也保持单屏。
- 宽屏本地与远程左栏在相同位置复用固定的“本地 / Remote”分段选择器,选中态由当前路由推导。
- 来源选择器使用独立来源切换动作,不复用单屏 `enterCodeEntry()`;具体会话行只选择当前来源内的会话。
- 宽屏来源切换使用 `clear(false)` 与 `pushPath(..., false)` 无转场替换路由,不播放页面进入或退出动画。
- 宽屏本地和远程 master pane 均不提供全局侧边栏入口;远程宽屏标题栏已移除会叠加本地 drawer 的菜单按钮,单屏标题栏菜单保持不变。
- 本地与远程来源切换使用路由合同计算目标,不累积跨来源返回路径。
- 切回本地时停止手机侧远程轮询,但不清空远程活动会话或断开桌面。
- 通过 `display.getCurrentFoldCreaseRegion()` 读取折痕;第一道有效纵向折痕用于 master/detail 分界,无有效折痕时回退到 `344`。
- 双折叠展开打开文件预览时,会话/预览分界对齐那一道折痕,而不是按视口对半切。
- 两道折痕或超宽宽度被识别为 extra-wide,但继续使用同一个 master-detail,不创建第三栏。
- 第二道折痕会把 detail 划分为无折痕候选内容带;会话视图选择最宽的候选带,同宽时优先右侧,避免 Composer、菜单和消息内操作热区跨越折痕。
- extra-wide detail 外层仍覆盖中间和右侧两屏;上述内容带只是 presentation 几何约束,不增加第三个业务 pane。
Expand Down Expand Up @@ -76,7 +78,7 @@ HarmonyOS 手机在展开双屏、完整展开三屏或其他宽屏形态下,
- 不改变远程连接、配对、鉴权或断线重连协议。
- 不因为切换会话来源而停止桌面端正在执行的任务。
- 第一阶段不调整 `RemoteCreate` 的页面结构。
- 不改变当前宽屏阈值和折叠状态判定规则
- 宽度阈值(720 / 1080)和合盖强制单屏保持不变;展开后是否进入宽屏由纵向折痕与设备类型共同决定
- 不因为三屏增加一个独立的第三栏、第二套会话列表或新的业务路由模式。

## 当前逻辑
Expand All @@ -86,13 +88,14 @@ HarmonyOS 手机在展开双屏、完整展开三屏或其他宽屏形态下,
`AppRootPresentation.isWideLayout()` 当前遵循以下逻辑:

```text
设备处于折叠状态 -> 单屏
设备非折叠,且 media query 命中宽屏 -> 宽屏
设备非折叠,且实际宽度达到阈值 -> 宽屏
其他 -> 单屏
设备处于折叠状态 -> 单屏
设备非折叠,宽视口,有可见纵向折痕,且放得下两栏 -> 宽屏(双折/三折展开)
设备非折叠,宽视口,折叠状态为展开/半折,即使折痕缺失 -> 宽屏(双折展开兜底)
设备非折叠,宽视口,无折痕,且设备类型为平板 -> 宽屏
其他 -> 单屏
```

折叠状态优先于宽度。因此即使报告的可用宽度较大,只要设备处于折叠状态,也必须继续使用单屏布局。
折叠状态优先于宽度和折痕。因此即使报告的可用宽度较大,只要设备处于折叠状态,也必须继续使用单屏布局。双折叠展开的一道折痕与三折叠的两道折痕使用同一套 master-detail,不新增第三会话栏

### 路由分流

Expand Down
43 changes: 25 additions & 18 deletions src/apps/mobile/harmonyos/entry/src/main/ets/i18n/RemoteI18n.ets
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const ZH_CN_MESSAGES: [string, string][] = [

['chatHome.prompt', '今天想做什么?'],
['chatHome.placeholder', '问问 BitFun'],
['chatHome.localHint', '这是手机上的对话。要读写电脑上的项目,请切换到远程。'],
['chatHome.research', '查找资料'],
['chatHome.researchPrompt', '帮我查找资料并整理重点:'],
['chatHome.image', '分析图片'],
Expand All @@ -35,12 +36,9 @@ const ZH_CN_MESSAGES: [string, string][] = [
['chatHome.organizePrompt', '请帮我梳理这个问题的重点和思路:'],
['chatHome.plan', '制定行动计划'],
['chatHome.planPrompt', '请帮我制定一份清晰、可执行的行动计划:'],
['chatHome.codeHintTitle', '这个任务可能需要 Code 能力'],
['chatHome.codeHintBody', '连接本地工作区后,BitFun 才能读取项目文件、运行命令并修改代码。'],
['chatHome.enterCode', '进入 Code'],
['chatHome.generalAdvice', '仅给出通用建议'],
['chatHome.generalPending', '普通聊天服务即将接入。需要处理本地项目时,请从侧栏进入 Code。'],
['chatHome.generalAdvicePending', '普通代码建议服务即将接入。需要读取项目或运行命令时,请进入 Code。'],
['chatHome.codeHintTitle', '这个任务可能需要远程能力'],
['chatHome.codeHintBody', '连接桌面工作区后,BitFun 才能读取项目文件、运行命令并修改代码。'],
['chatHome.enterCode', '进入远程'],

['generalChat.ready', 'BitFun 已就绪'],
['generalChat.generating', 'BitFun 正在回复'],
Expand All @@ -49,11 +47,10 @@ const ZH_CN_MESSAGES: [string, string][] = [
['generalChat.stopped', '已停止生成。'],
['generalChat.interrupted', '回复已中断,已保留收到的内容。'],
['generalChat.replyInterrupted', '回复中断'],
['generalChat.fileDownloadMock', '普通聊天暂不支持下载桌面端文件,请进入 Code 后处理本地文件。'],
['generalChat.filePreviewUnavailable', '普通聊天暂不支持预览桌面端文件,请进入 Code 后打开。'],
['generalChat.fileDownloadMock', '普通聊天暂不支持下载桌面端文件,请进入远程后处理本地文件。'],
['generalChat.filePreviewUnavailable', '普通聊天暂不支持预览桌面端文件,请进入远程后打开。'],
['generalChat.localRestoreFailed', '普通对话历史暂时无法恢复,你仍可新建对话。'],
['generalChat.modelNotConfigured', '请先在设置中配置普通对话模型。'],
['generalChat.imageNotSupported', '当前模型通道暂不支持图片,请先发送文字消息。'],
['generalChat.authenticationFailed', 'API Key 无效或无权访问该模型。'],
['generalChat.rateLimited', '模型服务请求过于频繁,请稍后重试。'],
['generalChat.serviceUnavailable', '模型服务暂时不可用,请稍后重试。'],
Expand Down Expand Up @@ -102,12 +99,13 @@ const ZH_CN_MESSAGES: [string, string][] = [
['sidebar.projects', '项目'],
['sidebar.tasks', '已计划'],
['sidebar.tools', '应用'],
['sidebar.code', 'Remote'],
['sidebar.code', '远程'],
['sidebar.more', '更多'],
['sidebar.recent', '最近'],
['sidebar.newChat', '聊天'],
['sidebar.generalChat', '普通对话'],
['sidebar.local', '本地'],
['sidebar.pinned', '置顶'],
['sidebar.archived', '已归档'],
['sidebar.archive', '归档'],
['sidebar.unarchive', '取消归档'],
Expand Down Expand Up @@ -146,18 +144,18 @@ const ZH_CN_MESSAGES: [string, string][] = [
['viewSettings.status', '运行状态'],
['common.unknown', '未知'],

['code.title', 'BitFun Code'],
['code.title', 'BitFun 远程'],
['code.heroTitle', '本地开发助手'],
['code.heroBody', '连接桌面端后,BitFun 可以访问你的代码仓库、运行命令和修改文件。'],
['code.connectWorkspace', '连接本地工作区'],
['code.connectDesc', '扫码或粘贴连接链接'],
['code.noWorkspacePreview', '连接桌面端后会显示最近打开的代码仓库。'],
['code.newTask', '新建 Code 任务'],
['code.newTask', '新建远程任务'],
['code.switchWorkspace', '切换工作区'],
['code.disconnect', '断开连接'],
['code.recentSessions', '最近 Code 会话'],
['code.emptySessionTitle', '暂无 Code 会话'],
['code.emptySessionText', '连接本地工作区后,可以新建 Code 任务处理项目问题。'],
['code.recentSessions', '最近远程会话'],
['code.emptySessionTitle', '暂无远程会话'],
['code.emptySessionText', '连接本地工作区后,可以新建远程任务处理项目问题。'],

['remote.title', '远程'],
['remote.chats', '聊天'],
Expand Down Expand Up @@ -275,7 +273,7 @@ const ZH_CN_MESSAGES: [string, string][] = [
['connect.scanPairCode', '扫描二维码以配对'],
['connect.scanPairCodeAction', '扫描二维码连接'],
['connect.accountDevicesTitle', '选择桌面设备'],
['connect.accountDevicesSubtitle', 'Remote'],
['connect.accountDevicesSubtitle', '远程'],
['connect.accountDevicesBody', '选择一台在线桌面继续工作。'],
['connect.availableDevices', '账号设备'],
['connect.deviceLastUsed', '上次连接'],
Expand Down Expand Up @@ -370,7 +368,7 @@ const ZH_CN_MESSAGES: [string, string][] = [
['home.emptyAssistantTitle', '暂无助理会话'],
['home.emptyAssistantText', '桌面端开放创建后可在这里开始。'],
['home.emptyTitle', '暂无会话'],
['home.emptyText', '用 Code 处理编码任务,或用 Cowork 处理日常办公。'],
['home.emptyText', '用远程处理编码任务,或用普通对话处理日常问题。'],

['sheet.createTitle', '创建 {0} 会话'],
['sheet.workspace', '工作区'],
Expand All @@ -382,9 +380,18 @@ const ZH_CN_MESSAGES: [string, string][] = [
['sheet.creating', '创建中...'],
['sheet.start', '开始'],

['chat.remoteSession', 'Remote Session'],
['chat.remoteSession', '远程会话'],
['chat.sessionTitle', '会话标题'],
['chat.sessionSection', '会话'],
['chat.pin', '置顶'],
['chat.unpin', '取消置顶'],
['chat.uploadedFiles', '已上传的文件'],
['chat.stop', '停止'],
['chat.send', '发送'],
['chat.voiceInput', '语音输入'],
['chat.stopListening', '停止听写'],
['chat.addImage', '添加图片'],
['chat.removeImage', '移除图片'],
['chat.selectModel', '选择模型'],
['chat.loadOlder', '加载更早消息'],
['chat.inputPlaceholder', '向 BitFun 提问'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ export interface ChatMessageItemResponse {
export interface ImageAttachment {
name: string;
data_url: string;
mime_type?: string;
}

export interface SelectedImageAttachment extends ImageAttachment {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RemotePermissionMode, RemoteSession } from '../../model/RemoteModels';
import { CloudAccountDevice } from '../../services/CloudAccountClient';
import { DetectedUrlAction } from '../../services/ConnectScanDecisionPolicy';
import { ConversationIntent } from './ConversationIntent';
import { AppRoute, ConversationSource } from '../navigation/AppRouteContract';

Expand Down Expand Up @@ -88,6 +89,7 @@ export interface SettingsPresentationActions {
readonly cloudLogin: (relayUrl: string, username: string, password: string) => Promise<string>;
readonly cloudLogout: () => Promise<void>;
readonly cloudListDevices: () => Promise<CloudAccountDevice[]>;
readonly cloudSelectDevice: (device: CloudAccountDevice) => Promise<void>;
readonly getPermissionMode: () => Promise<RemotePermissionMode>;
readonly setPermissionMode: (mode: RemotePermissionMode) => Promise<RemotePermissionMode>;
readonly testGeneral: (url: string, key: string, model: string, clear: boolean) => Promise<string>;
Expand All @@ -100,7 +102,7 @@ export interface ConnectPresentationActions {
readonly clearPairing: () => void;
readonly urlChanged: (url: string) => void;
readonly userChanged: (user: string) => void;
readonly detected: (url: string) => boolean;
readonly detected: (url: string) => string;
readonly inputVisible: (visible: boolean) => void;
readonly paste: () => void;
readonly scan: () => void;
Expand Down Expand Up @@ -136,13 +138,14 @@ export function emptyAppRootPresentationActions(): AppRootPresentationActions {
onSettings: {
close: () => {}, addConnection: () => {}, disconnect: () => {}, reconnect: () => {}, openAccount: () => {},
cloudLogin: async () => '', cloudLogout: async () => {},
cloudListDevices: async () => [], getPermissionMode: async () => 'ask',
cloudListDevices: async () => [], cloudSelectDevice: async () => {},
getPermissionMode: async () => 'ask',
setPermissionMode: async (mode: RemotePermissionMode) => mode,
testGeneral: async () => '', saveGeneral: async () => ''
},
onConnect: {
back: () => {}, connect: () => {}, clearPairing: () => {}, urlChanged: () => {}, userChanged: () => {},
detected: () => false, inputVisible: () => {}, paste: () => {}, scan: () => {},
detected: () => DetectedUrlAction.INVALID, inputVisible: () => {}, paste: () => {}, scan: () => {},
cloudListDevices: async () => [], cloudSelectDevice: async () => {}
},
onFilePreview: { close: () => {}, refresh: () => {}, download: () => {}, openLink: () => {} },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConversationUiQuestionAnswer } from '../components/ConversationUiModels';
import { ConversationUiQuestionAnswer } from '../state/ConversationUiModels';
import { FilePreviewRequest } from '../../model/FilePreviewTarget';

export enum ConversationIntentType {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RemoteQuestionAnswerPayload, RemoteSession } from '../../model/RemoteModels';
import { AppRoute, AppRouteContract } from '../navigation/AppRouteContract';
import { ConversationIntent, ConversationIntentType } from './ConversationIntent';
import { toRemoteQuestionAnswer } from '../components/ConversationUiModels';
import { toRemoteQuestionAnswer } from '../state/ConversationUiModels';
import { FilePreviewRequest } from '../../model/FilePreviewTarget';

export interface ConversationIntentDispatcherHooks {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export struct AppSettingsSurface {
cloudLogin: this.actions.onSettings.cloudLogin,
cloudLogout: this.actions.onSettings.cloudLogout,
cloudListDevices: this.actions.onSettings.cloudListDevices,
cloudSelectDevice: this.actions.onSettings.cloudSelectDevice,
getPermissionMode: this.actions.onSettings.getPermissionMode,
setPermissionMode: this.actions.onSettings.setPermissionMode,
openAccountOnAppear: this.shellState.settingsMode === 'account',
Expand Down
Loading