Skip to content
16 changes: 16 additions & 0 deletions src/apps/mobile/harmonyos/entry/build-profile.json5
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@
},
{
"name": "ohosTest",
},
{
// Emulator-only target. The x86_64 emulator cannot install the shipped
// arm64-only `default` artifact, so a separate target carries the ABI
// override. `default` stays arm64-only: nothing else may add x86_64 to it.
// Build with `-p module=entry@emulator`.
"name": "emulator",
"config": {
"buildOption": {
"externalNativeOptions": {
"abiFilters": [
"x86_64"
]
}
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class EntryAbility extends UIAbility {

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const scenarioId = want.parameters?.['openbitfunDesignPreview'];
if (scenarioId === 'history-scroll' || scenarioId === 'durable-timeline' || scenarioId === 'composer-submit' || scenarioId === 'catalog-refresh' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'device-selector' || scenarioId === 'device-selector-dark' || scenarioId === 'welcome-home' || scenarioId === 'connected-conversation' ||
if (scenarioId === 'history-scroll' || scenarioId === 'durable-timeline' || scenarioId === 'composer-submit' || scenarioId === 'catalog-refresh' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector' || scenarioId === 'model-role-selector-dark' || scenarioId === 'model-role-selector-wide' || scenarioId === 'subagent-process' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-wide' || scenarioId === 'immersive-bottom-drawer' || scenarioId === 'immersive-bottom-settings' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'immersive-bottom-cover' || scenarioId === 'immersive-bottom-miniapps' || scenarioId === 'immersive-bottom-wide-shell' || scenarioId === 'immersive-bottom-tools' || scenarioId === 'immersive-bottom-tools-editor' || scenarioId === 'immersive-bottom-tools-file-action' || scenarioId === 'immersive-bottom-view-settings' || scenarioId === 'immersive-bottom-connect' || scenarioId === 'composer-overflow' || scenarioId === 'composer-overflow-empty' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-wide' || scenarioId === 'composer-overflow-expanded' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector' || scenarioId === 'device-selector-dark' || scenarioId === 'welcome-home' || scenarioId === 'connected-conversation' ||
scenarioId === 'streaming-dark' ||
scenarioId === 'reconnecting-wide' || scenarioId === 'narrow-multiline' ||
scenarioId === 'fold-context' || scenarioId === 'long-reading' ||
Expand All @@ -25,9 +25,9 @@ export default class EntryAbility extends UIAbility {
AppStorage.setOrCreate('scenarioId', scenarioId);
}
try {
const colorMode = scenarioId === 'cold-start-home-dark' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'device-selector-dark' || scenarioId === 'streaming-dark' || scenarioId === 'interaction-mailbox-dark'
const colorMode = scenarioId === 'cold-start-home-dark' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector-dark' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector-dark' || scenarioId === 'streaming-dark' || scenarioId === 'interaction-mailbox-dark'
? ConfigurationConstant.ColorMode.COLOR_MODE_DARK
: scenarioId === 'cold-start-home' || scenarioId === 'catalog-refresh' || scenarioId === 'device-selector' || scenarioId === 'connected-conversation' || scenarioId === 'reconnecting-wide'
: scenarioId === 'cold-start-home' || scenarioId === 'catalog-refresh' || scenarioId === 'model-role-selector' || scenarioId === 'model-role-selector-wide' || scenarioId === 'subagent-process' || scenarioId === 'file-visibility' || scenarioId === 'immersive-bottom' || scenarioId === 'immersive-bottom-wide' || scenarioId === 'immersive-bottom-drawer' || scenarioId === 'immersive-bottom-settings' || scenarioId === 'immersive-bottom-cover' || scenarioId === 'immersive-bottom-miniapps' || scenarioId === 'immersive-bottom-wide-shell' || scenarioId === 'immersive-bottom-tools' || scenarioId === 'immersive-bottom-tools-editor' || scenarioId === 'immersive-bottom-tools-file-action' || scenarioId === 'immersive-bottom-view-settings' || scenarioId === 'immersive-bottom-connect' || scenarioId === 'composer-overflow' || scenarioId === 'composer-overflow-empty' || scenarioId === 'composer-overflow-wide' || scenarioId === 'composer-overflow-expanded' || scenarioId === 'device-selector' || scenarioId === 'connected-conversation' || scenarioId === 'reconnecting-wide'
? ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
: ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
this.context.getApplicationContext().setColorMode(colorMode);
Expand Down Expand Up @@ -63,7 +63,11 @@ export default class EntryAbility extends UIAbility {
try {
const mainWindow = windowStage.getMainWindowSync();
this.mainWindow = mainWindow;
mainWindow.setWindowLayoutFullScreen(false);
// The shell runs immersive: the chat page's transcript has to reach the
// bottom edge of the screen, and pages read the avoid areas themselves to
// keep their content clear of the system bars.
mainWindow.setWindowLayoutFullScreen(true);
this.logWindowInsets(mainWindow);
this.updateSystemBars(this.context.config?.colorMode);
} catch (err) {
hilog.error(DOMAIN, TAG, 'Failed to configure window bars. Cause: %{public}s', JSON.stringify(err));
Expand Down Expand Up @@ -102,6 +106,24 @@ export default class EntryAbility extends UIAbility {
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onBackground');
}

/**
* The immersive shell is not verifiable from a screenshot alone: whether the
* window covers the status bar and how tall the bottom strip is are device
* facts, so state them once per launch.
*/
private logWindowInsets(mainWindow: window.Window): void {
try {
const system = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const navigation = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
hilog.info(DOMAIN, TAG,
'Window insets statusBar=%{public}s navigation=%{public}s windowRect=%{public}s',
String(system.topRect.height), String(navigation.bottomRect.height),
JSON.stringify(mainWindow.getWindowProperties().windowRect));
} catch (err) {
hilog.error(DOMAIN, TAG, 'Failed to read window insets. Cause: %{public}s', JSON.stringify(err));
}
}

private updateSystemBars(colorMode?: ConfigurationConstant.ColorMode): void {
if (!this.mainWindow) {
return;
Expand All @@ -111,8 +133,12 @@ export default class EntryAbility extends UIAbility {
const content = dark ? MobileDesignColors.ink.dark : MobileDesignColors.ink.light;
try {
this.mainWindow.setWindowSystemBarProperties({
// The status bar keeps the page colour it has always had — the window
// does not draw behind it on every device, and an opaque bar cannot
// regress the header band either way. The navigation bar is transparent
// instead: the chat page's bottom fade is what should be visible there.
statusBarColor: background,
navigationBarColor: background,
navigationBarColor: MobileDesignColors.transparent.light,
statusBarContentColor: content,
navigationBarContentColor: content
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,11 @@ export const EN_US_MESSAGES: [string, string][] = [
['chat.addImage', 'Add image'],
['chat.removeImage', 'Remove image'],
['chat.selectModel', 'Select model'],
['chat.modelPrimary', 'Primary Model'],
['chat.modelFast', 'Fast Model'],
['chat.modelFastNotConfigured', 'Not set, falls back to primary'],
['chat.modelRolePrimary', 'Primary'],
['chat.modelRoleFast', 'Fast'],
['chat.loadOlder', 'Load older messages'],
['chat.loadOlderFailed', 'Could not load earlier messages. Tap to retry.'],
['chat.inputPlaceholder', 'Ask OpenBitFun'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,11 @@ export const ZH_CN_MESSAGES: [string, string][] = [
['chat.addImage', '添加图片'],
['chat.removeImage', '移除图片'],
['chat.selectModel', '选择模型'],
['chat.modelPrimary', '主力模型'],
['chat.modelFast', '快速模型'],
['chat.modelFastNotConfigured', '未配置时使用主力模型'],
['chat.modelRolePrimary', '主力'],
['chat.modelRoleFast', '快速'],
['chat.loadOlder', '加载更早消息'],
['chat.loadOlderFailed', '加载更早消息失败,点按重试。'],
['chat.inputPlaceholder', '向 OpenBitFun 提问'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ export struct AppShell {
.backgroundColor(PAGE_BG)
.borderRadius(MobileDesignGeometry.sheetTopRadius)
.clip(true)
// The cover is bound with `enableSafeArea: true`, so the framework already
// ends its page above the navigation bar and this stays the design's own
// float above that edge.
.margin({ bottom: MobileDesignGeometry.loginSheetOuterMargin })
}
.width('100%')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { SidebarToggleButton } from './SidebarToggleButton';
import { SidebarGlyph } from './SidebarGlyphs';
import { SidebarConnectionActionPolicy } from '../policy/SidebarConnectionActionPolicy';
import { WindowInsetsBinding } from '../../services/WindowSystemBarService';

/** Device and workspace navigation shared by compact and wide controllers. */
@ComponentV2
Expand Down Expand Up @@ -47,6 +48,14 @@ export struct AppSidebar {
@Event onOpenAccount: () => void = () => {};
@Local showSearch: boolean = false;
@Local sessionSearchQuery: string = '';
/**
* The window's own insets, in vp. The panel's fill runs to its bottom edge —
* the screen edge for the compact drawer — and the session rows scroll under
* the navigation indicator, so the strip is the list's own business now. The
* floating footer keeps its distance from that edge itself, which is what
* keeps the buttons above the bar while the rows run under it.
*/
@Local insets: WindowInsetsBinding = new WindowInsetsBinding();
/**
* The authenticated device/workspace section.
*
Expand All @@ -55,6 +64,17 @@ export struct AppSidebar {
*/
@BuilderParam contentSlot: () => void = this.EmptySlot;

aboutToAppear(): void {
const context = this.getUIContext().getHostContext();
if (context) {
this.insets.bind(this.getUIContext(), context);
}
}

aboutToDisappear(): void {
this.insets.unbind();
}

build() {
this.SidebarContent()
}
Expand Down Expand Up @@ -95,7 +115,7 @@ export struct AppSidebar {
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding({ bottom: this.usesCompactFooter() ? 84 : 120 })
.padding({ bottom: this.scrollTailPadding() })
}
.width('100%')
.height('100%')
Expand All @@ -106,6 +126,8 @@ export struct AppSidebar {
// Pre-connection actions form a conventional footer and retain a fade.
// Once either account or QR access unlocks the workspace, navigation
// uses two floating controls and the scroll stays visible behind them.
// The fade stops above the navigation bar with the controls it serves,
// so the rows it does not cover stay readable in the strip below it.
if (!this.shouldShowPrimaryNavigation()) {
Column()
.width('100%')
Expand All @@ -115,6 +137,7 @@ export struct AppSidebar {
angle: 180,
colors: [[SIDEBAR_BG_FADE, 0.0], [SIDEBAR_BG, 0.62]]
})
.margin({ bottom: this.footerBottomPadding() })
}

if (this.shouldShowPrimaryNavigation()) {
Expand All @@ -128,7 +151,12 @@ export struct AppSidebar {
}
.width('100%')
.height('100%')
.padding({ left: 20, right: 20, top: 0, bottom: 16 })
// The panel's fill runs to its own bottom edge — the screen edge for the
// compact drawer, the wide master pane's card edge in the wide host — and
// the session rows are content that scrolls under the navigation indicator.
// The footer, its fade and the list's tail are what keep the strip clear,
// each against the edge this panel actually has.
.padding({ left: 20, right: 20, top: 0 })
.backgroundColor(SIDEBAR_BG)
}

Expand Down Expand Up @@ -256,6 +284,7 @@ export struct AppSidebar {
onOpenTools: () => this.requestDeviceTools(),
onOpenSettings: () => this.onOpenSettings()
})
.margin({ bottom: this.footerBottomPadding() })
}

@Builder
Expand All @@ -279,6 +308,7 @@ export struct AppSidebar {
})
}
.width('100%')
.margin({ bottom: this.footerBottomPadding() })
.zIndex(2)
}

Expand All @@ -298,4 +328,25 @@ export struct AppSidebar {
private usesCompactFooter(): boolean {
return this.shouldShowPrimaryNavigation() || this.connectionState === 'connected';
}

/**
* The distance the floating footer and its fade keep from the panel's own
* bottom edge: the design's spacing where the strip is already clear of it,
* the strip itself where it is not. In the compact drawer that edge is the
* screen edge, so the buttons sit directly above the navigation bar; in the
* wide master pane the edge is the floating card's, and the same number
* keeps the buttons clear of the bar there too.
*/
private footerBottomPadding(): number {
return this.insets.bottomPadding(16);
}

/**
* The session list's tail spacer: the floating footer's clearance plus the
* strip. The rows roll under the footer and the navigation indicator while
* they are moving, and the last row still rests above the bar.
*/
private scrollTailPadding(): number {
return (this.usesCompactFooter() ? 84 : 120) + this.insets.bottom;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,30 @@ export struct ChatMessageBubble {
this.AssistantBubble()
}

/**
* The Task branch as the transcript currently holds it.
*
* The structured ForEach that draws the card is keyed by the branch view key,
* which deliberately does not change while the subagent publishes steps, so its
* item builder keeps the entry it captured — the empty one a Task has before
* its subagent starts. Param expressions are re-evaluated on every render, so
* the branch is re-read from the observed message here, and a step that arrives
* later reaches the card without recreating it or closing it.
*
* Known limits. A branch nested inside another branch's child items resolves
* only through its captured entry: the lookup scans the top-level transcript,
* and today's wire publishes subagent records flat, so that shape is not
* reachable yet. The lookup also runs once per card param — title, status and
* items — which is bounded by the few branches a turn can draw.
*/
private currentSubagentBranch(entry: ConversationUiMessageItem, path: string): ConversationUiMessageItem {
return ChatMessageStructurePolicy.subagentBranchAt(
this.structuredProcessItems(this.item.items || []),
ChatMessageStructurePolicy.subagentOrdinal(path),
entry.tool?.id || ''
) || entry;
}

@Builder
AssistantBubble() {
Row() {
Expand Down Expand Up @@ -340,10 +364,11 @@ export struct ChatMessageBubble {
SubagentGroup(entry: ConversationUiMessageItem, path: string, activeScope: boolean = false) {
SubagentTaskCard({
interactionMailbox: this.interactionMailbox,
title: this.subagentTitle(entry),
title: this.subagentTitle(this.currentSubagentBranch(entry, path)),
path: path,
status: entry.tool?.status || (activeScope ? 'running' : 'completed'),
items: entry.subItems || [],
status: this.currentSubagentBranch(entry, path).tool?.status ||
(activeScope ? 'running' : 'completed'),
items: this.currentSubagentBranch(entry, path).subItems || [],
renderRevision: this.renderRevision,
onApproveTool: this.onApproveTool,
onRejectTool: this.onRejectTool,
Expand Down
Loading
Loading