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
18 changes: 18 additions & 0 deletions src/apps/mobile/harmonyos/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ source scripts/ohos-env.sh

## Responsive interaction semantics

- Every HarmonyOS change that can affect presentation or interaction — including
state, actions, ViewModels, and components — must treat compact phones, wide
screens, and foldables in both folded and unfolded postures as first-class
targets. Do not implement or review a feature against only the currently
visible layout.
- Before editing, search for every responsive host and presentation branch that
exposes the affected action or state (for example compact overlays, wide
master/detail hosts, sidebars, routed destinations, sheets, and popovers).
Keep their behavior and capability gating aligned; a fix is incomplete while
an equivalent wide or compact entry point still uses legacy behavior.
- Do not infer device class from a single width captured at startup. Layout must
remain correct when a foldable changes posture or window size while the app is
running, preserving state, selection, and interaction meaning across the
transition.
- For UI changes, verify at minimum one compact layout and one wide layout. When
a foldable or resizable emulator/device is available, also exercise a live
folded/unfolded or resize transition. Record any unverified posture explicitly
in the handoff instead of treating a phone-only check as sufficient evidence.
- 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export const EN_US_MESSAGES: [string, string][] = [
['remote.pickSession', 'Choose a session'],
['remote.pickSessionText', 'Open a session from the sidebar, or create one.'],
['remote.startSession', 'New session'],
['remote.harness.title', 'Choose execution mode'],
['remote.harness.minimal', 'Minimal'],
['remote.harness.standard', 'Standard'],
['remote.harness.ultimate', 'Ultimate'],
['remote.connectTitle', 'Connect desktop'],
['remote.connectText', 'Scan the QR code on the desktop to start remote work.'],
['remote.actions', 'Remote settings'],
Expand Down Expand Up @@ -456,6 +460,10 @@ export const EN_US_MESSAGES: [string, string][] = [
['chat.approve', 'Approve'],
['chat.reject', 'Reject'],
['chat.cancelTool', 'Cancel tool'],
['chat.planView', 'View plan'],
['chat.planBuild', 'Build Plan'],
['chat.planBuildUnavailable', 'Update the desktop app to build plans from mobile.'],
['chat.planBuildWaitForTurn', 'Wait for the current task to finish before building this plan.'],
['chat.toolInput', 'Tool input'],
['chat.reset', 'Reset'],
['chat.editJsonInput', 'Edit JSON input'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export const ZH_CN_MESSAGES: [string, string][] = [
['remote.pickSession', '选择一个会话'],
['remote.pickSessionText', '从侧边栏打开会话,或新建一个。'],
['remote.startSession', '新建会话'],
['remote.harness.title', '选择执行模式'],
['remote.harness.minimal', '极简'],
['remote.harness.standard', '标准'],
['remote.harness.ultimate', '极致'],
['remote.connectTitle', '连接桌面端'],
['remote.connectText', '扫描桌面端显示的二维码,开始远程处理任务。'],
['remote.actions', '远程设置'],
Expand Down Expand Up @@ -456,6 +460,10 @@ export const ZH_CN_MESSAGES: [string, string][] = [
['chat.approve', '批准'],
['chat.reject', '拒绝'],
['chat.cancelTool', '取消工具'],
['chat.planView', '查看计划'],
['chat.planBuild', '构建 Plan'],
['chat.planBuildUnavailable', '请更新桌面端后再从手机构建 Plan。'],
['chat.planBuildWaitForTurn', '请等待当前任务结束后再构建这个 Plan。'],
['chat.toolInput', '工具输入'],
['chat.reset', '重置'],
['chat.editJsonInput', '编辑 JSON 输入'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export interface RemoteCommand {
known_msg_count?: number;
known_model_catalog_version?: number;
turn_id?: string;
display_content?: string;
plan_file_path?: string;
plan_name?: string;
tool_id?: string;
model_id?: string;
reason?: string;
Expand Down Expand Up @@ -290,6 +293,22 @@ export interface SendMessageResponse extends CommandStatusResponse {
turn_id?: string;
}

export interface SteerTurnResponse extends CommandStatusResponse {
session_id?: string;
turn_id?: string;
steering_id?: string;
}

export interface SteerTurnResult {
sessionId: string;
turnId: string;
steeringId: string;
}

export const REMOTE_CAPABILITY_DIALOG_STEER_V1: string = 'dialog_steer_v1';
export const REMOTE_CAPABILITY_PLAN_BUILD_V1: string = 'plan_build_v1';
export const REMOTE_CAPABILITY_HARNESS_PROFILES_V1: string = 'harness_profiles_v1';

export interface RemoteModelConfig {
id: string;
name: string;
Expand Down Expand Up @@ -360,6 +379,13 @@ export interface RemoteToolStatusResponse {
result_preview?: string;
error_preview?: string;
exit_code?: number;
plan?: RemotePlanToolResponse;
}

export interface RemotePlanToolResponse {
file_path: string;
name: string;
overview?: string;
}

export interface RemoteQuestionAnswerPayload {
Expand All @@ -372,10 +398,13 @@ export interface RemoteQuestionAnswerPayload {

export interface ChatMessageItemResponse {
type?: string;
steering_id?: string;
round_index?: number;
content?: string;
tool?: RemoteToolStatusResponse;
is_subagent?: boolean;
subItems?: ChatMessageItemResponse[];
images?: ImageAttachment[];
}

export interface ImageAttachment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,47 @@ export enum ConversationIntentType {
RemoveImage = 'remove_image',
OpenFilePreview = 'open_file_preview',
DownloadFile = 'download_file',
BuildPlan = 'build_plan',
Send = 'send',
VoiceInput = 'voice_input',
ChatInputChanged = 'chat_input_changed'
}

export class PlanBuildRequest {
readonly path: string;
readonly name: string;

constructor(path: string, name: string) {
this.path = path;
this.name = name;
}
}

export class ConversationIntent {
readonly type: ConversationIntentType;
readonly value: string;
readonly toolId: string;
readonly updatedInput?: Object;
readonly answers?: ConversationUiQuestionAnswer;
readonly filePreviewRequest?: FilePreviewRequest;
readonly planBuildRequest?: PlanBuildRequest;

constructor(
type: ConversationIntentType,
value: string = '',
toolId: string = '',
updatedInput?: Object,
answers?: ConversationUiQuestionAnswer,
filePreviewRequest?: FilePreviewRequest
filePreviewRequest?: FilePreviewRequest,
planBuildRequest?: PlanBuildRequest
) {
this.type = type;
this.value = value;
this.toolId = toolId;
this.updatedInput = updatedInput;
this.answers = answers;
this.filePreviewRequest = filePreviewRequest;
this.planBuildRequest = planBuildRequest;
}
}

Expand Down Expand Up @@ -84,4 +98,16 @@ export class ConversationIntents {
new FilePreviewRequest(reference, label)
);
}

static buildPlan(path: string, name: string): ConversationIntent {
return new ConversationIntent(
ConversationIntentType.BuildPlan,
'',
'',
undefined,
undefined,
undefined,
new PlanBuildRequest(path, name)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ConversationIntentDispatcherHooks {
readonly removeImage: (id: string) => void;
readonly openFilePreview: (route: AppRoute, request: FilePreviewRequest) => void;
readonly downloadFile: (path: string) => void;
readonly buildPlan: (path: string, name: string) => Promise<void>;
readonly send: () => Promise<void>;
readonly voiceInput: () => Promise<void>;
readonly inputChanged: (route: AppRoute, value: string) => void;
Expand Down Expand Up @@ -81,6 +82,11 @@ export class ConversationIntentDispatcher {
case ConversationIntentType.OpenFilePreview:
if (intent.filePreviewRequest) this.hooks.openFilePreview(route, intent.filePreviewRequest); return;
case ConversationIntentType.DownloadFile: this.hooks.downloadFile(intent.value); return;
case ConversationIntentType.BuildPlan:
if (route === AppRoute.RemoteChat && intent.planBuildRequest) {
void this.hooks.buildPlan(intent.planBuildRequest.path, intent.planBuildRequest.name);
}
return;
case ConversationIntentType.Send: void this.hooks.send(); return;
case ConversationIntentType.VoiceInput: void this.hooks.voiceInput(); return;
case ConversationIntentType.ChatInputChanged: this.hooks.inputChanged(route, intent.value); return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { GeneralChatPageState } from '../state/GeneralChatPageState';
import { DeviceDirectoryState } from '../state/DeviceDirectoryState';
import { RemotePageState } from '../state/RemotePageState';
import { REMOTE_CAPABILITY_HARNESS_PROFILES_V1 } from '../../model/RemoteModels';
import { AppSidebar } from './AppSidebar';
import { ConnectView } from './ConnectView';
import { RemoteControlSettingsSheet } from './RemoteControlSettingsSheet';
Expand Down Expand Up @@ -79,11 +80,13 @@ export struct AppSidebarSurface {
this.remotePageState.desktopName,
selectedSessionId: this.remoteSelectedSessionId(),
runningSessionId: this.remoteRunningSessionId(),
supportsHarnessProfiles: this.remotePageState.supportsHostCapability(
REMOTE_CAPABILITY_HARNESS_PROFILES_V1),
workspacePickerPlacement: this.sessionDetailsPlacement,
onOpenSession: this.actions.onSidebar.openSession,
onCreateInWorkspace: (deviceId: string, path: string) => {
onCreateInWorkspace: (deviceId: string, path: string, agentType: string) => {
this.actions.onSidebar.close();
this.actions.onRemoteHome.createInWorkspace(path, 'code', deviceId);
this.actions.onRemoteHome.createInWorkspace(path, agentType, deviceId);
},
onOpenWorkspace: (deviceId: string, path: string) => {
this.actions.onRemoteHome.selectWorkspace(path, deviceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export struct ChatMessageBubble {
@Event onCopyMessage: (text: string) => void = (_text: string) => {};
@Event onRetryMessage: (text: string) => void = (_text: string) => {};
@Event onOpenFilePreview: (path: string, label: string) => void = (_path: string, _label: string) => {};
@Event onBuildPlan: (path: string, name: string) => void = (_path: string, _name: string) => {};
@Event onDownloadFile: (path: string) => void = (_path: string) => {};
private readonly fileReferenceCache: MessageFileReferenceProjectionCache =
new MessageFileReferenceProjectionCache();
Expand Down Expand Up @@ -302,7 +303,12 @@ export struct ChatMessageBubble {
if (this.isSubagentEntry(entry)) {
this.SubagentGroup(entry, path, childActiveScope)
} else {
if (this.isThinkingEntry(entry)) {
if (this.isUserSteeringEntry(entry)) {
ChatUserMessageBubble({
item: this.steeringMessage(entry, path),
showRetryAction: false
})
} else if (this.isThinkingEntry(entry)) {
this.Thinking(
entry.content || '',
statusOverride || this.item.status,
Expand Down Expand Up @@ -339,7 +345,8 @@ export struct ChatMessageBubble {
onRejectTool: this.onRejectTool,
onCancelTool: this.onCancelTool,
onAnswerQuestion: this.onAnswerQuestion,
onOpenFilePreview: this.onOpenFilePreview
onOpenFilePreview: this.onOpenFilePreview,
onBuildPlan: this.onBuildPlan
})
}

Expand Down Expand Up @@ -387,6 +394,9 @@ export struct ChatMessageBubble {
},
onOpenFilePreview: (path: string, label: string) => {
this.onOpenFilePreview(path, label);
},
onBuildPlan: (path: string, name: string) => {
this.onBuildPlan(path, name);
}
})
}
Expand Down Expand Up @@ -657,6 +667,21 @@ export struct ChatMessageBubble {
return ChatMessageStructurePolicy.isThinkingEntry(entry);
}

private isUserSteeringEntry(entry: ConversationUiMessageItem): boolean {
return ChatMessageStructurePolicy.isUserSteeringEntry(entry);
}

private steeringMessage(entry: ConversationUiMessageItem, path: string): ConversationUiMessage {
return {
id: entry.steering_id || `${this.item.id}-${path}-steering`,
role: 'user',
text: entry.content || '',
status: 'sent',
detail: '',
images: entry.images
};
}

private isTextEntry(entry: ConversationUiMessageItem): boolean {
return ChatMessageStructurePolicy.isTextEntry(entry);
}
Expand Down
Loading
Loading