Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,28 @@ export default class EntryAbility extends UIAbility {
private previewStorage?: LocalStorage;

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const scenarioId = want.parameters?.['openbitfunDesignPreview'];
// `want.parameters` is untyped, so the scenario id is narrowed to a string
// once here; every branch below then compares or matches on text.
const previewId = want.parameters?.['openbitfunDesignPreview'] as string | undefined;
const scenarioId: string = previewId ?? '';
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-drawer-dark' || 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' ||
scenarioId === 'cold-start-home' || scenarioId === 'cold-start-home-dark' ||
scenarioId === 'copy-selection' || scenarioId === 'copy-selection-dark' ||
scenarioId === 'copy-selection-nav' || scenarioId === 'copy-selection-wide' ||
scenarioId === 'interaction-mailbox' || scenarioId === 'interaction-mailbox-dark') {
scenarioId === 'interaction-mailbox' || scenarioId === 'interaction-mailbox-dark' ||
scenarioId.startsWith('command-palette')) {
this.initialPage = 'pages/preview/MobileDesignGallery';
this.previewStorage = new LocalStorage();
this.previewStorage.setOrCreate('scenarioId', scenarioId);
AppStorage.setOrCreate('scenarioId', scenarioId);
}
try {
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-drawer-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' || scenarioId === 'copy-selection-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-drawer-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' || scenarioId === 'copy-selection-dark' || (scenarioId.startsWith('command-palette') && scenarioId.endsWith('-dark'))
? ConfigurationConstant.ColorMode.COLOR_MODE_DARK
: 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' || scenarioId === 'copy-selection' || scenarioId === 'copy-selection-nav' || scenarioId === 'copy-selection-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' || scenarioId === 'copy-selection' || scenarioId === 'copy-selection-nav' || scenarioId === 'copy-selection-wide' || (scenarioId.startsWith('command-palette') && !scenarioId.endsWith('-dark'))
? ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
: ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
this.context.getApplicationContext().setColorMode(colorMode);
Expand Down
200 changes: 200 additions & 0 deletions src/apps/mobile/harmonyos/entry/src/main/ets/model/RemoteModels.ets
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,206 @@ export const REMOTE_CAPABILITY_PLAN_BUILD_V1: string = 'plan_build_v1';
export const REMOTE_CAPABILITY_HARNESS_PROFILES_V1: string = 'harness_profiles_v1';
/** The host resolves `workspace_id` in commands and stamps it on its projections. */
export const REMOTE_CAPABILITY_WORKSPACE_ID_REFERENCES_V1: string = 'workspace_id_references_v1';
/**
* The composer command palette has no capability of its own.
*
* Skills, compact, init and usage all reach the desktop through the existing
* `host_invoke` bridge, so the phone cannot ask for a capability that a host
* would have to be taught: it probes one of the verbs instead and degrades on
* the answer. Session goals are plain message text and need no host support at
* all. Only the skills picker's grouping is local, so these are display values.
*/
export const REMOTE_SKILL_SOURCE_BUILTIN: string = 'builtin';
export const REMOTE_SKILL_SOURCE_USER: string = 'user';
export const REMOTE_SKILL_SOURCE_PROJECT: string = 'project';

/**
* One palette entry, projected from the host's `get_mode_skill_configs` rows.
*
* The host payload is a `ModeSkillInfo`: a `SkillInfo` flattened beside the
* mode's availability flags. `id` is the skill key, because that is what the
* `[$key]` message token carries; the flags are kept on the entry so the picker
* can filter with host truth instead of guessing.
*/
export interface RemoteSkillCatalogEntry {
id: string;
name: string;
description: string;
/** Host group taxonomy; empty for user-authored skills. */
group: string;
/** `builtin`, `project` or `user`; the picker's fallback grouping. */
source: string;
/** Another skill of the same name wins in this mode. */
shadowed: boolean;
selectedForRuntime: boolean;
effectiveEnabled: boolean;
globallyEnabled: boolean;
allowUserInvocation: boolean;
}

export interface RemoteCommandCatalog {
skills: RemoteSkillCatalogEntry[];
}

/**
* Answer to `compact_session` / `run_init_agents_md`.
*
* Both handlers return `StartDialogTurnResponse`, so the accepted shape is the
* host's own success/message pair rather than a palette-specific envelope.
*/
export interface RemoteTurnAcceptedValue {
success?: boolean;
message?: string;
}

/**
* The session usage report, mirrored from the desktop contract.
*
* Every field is optional on purpose: an older or a partially instrumented host
* reports the subset it can measure, and the sheet renders exactly the rows the
* payload carries instead of inventing zeroes for what is missing. Both the
* desktop camelCase spelling and the snake_case spelling are accepted, because
* this payload crosses a relay between two independently built versions.
*/
export interface RemoteUsageWorkspace {
kind?: string;
path_label?: string;
pathLabel?: string;
workspace_id?: string;
workspaceId?: string;
}

export interface RemoteUsageScope {
kind?: string;
turn_count?: number;
turnCount?: number;
includes_subagents?: boolean;
includesSubagents?: boolean;
}

export interface RemoteUsageCoverage {
level?: string;
available?: string[];
missing?: string[];
notes?: string[];
}

export interface RemoteUsageTime {
accounting?: string;
denominator?: string;
wall_time_ms?: number;
wallTimeMs?: number;
active_turn_ms?: number;
activeTurnMs?: number;
model_ms?: number;
modelMs?: number;
tool_ms?: number;
toolMs?: number;
idle_gap_ms?: number;
idleGapMs?: number;
}

export interface RemoteUsageTokens {
source?: string;
input_tokens?: number;
inputTokens?: number;
output_tokens?: number;
outputTokens?: number;
total_tokens?: number;
totalTokens?: number;
cached_tokens?: number;
cachedTokens?: number;
cache_coverage?: string;
cacheCoverage?: string;
}

export interface RemoteUsageModelRow {
model_id?: string;
modelId?: string;
call_count?: number;
callCount?: number;
total_tokens?: number;
totalTokens?: number;
}

export interface RemoteUsageToolRow {
tool_name?: string;
toolName?: string;
category?: string;
call_count?: number;
callCount?: number;
error_count?: number;
errorCount?: number;
}

export interface RemoteUsageFiles {
scope?: string;
changed_files?: number;
changedFiles?: number;
added_lines?: number;
addedLines?: number;
deleted_lines?: number;
deletedLines?: number;
}

export interface RemoteUsageCompression {
compaction_count?: number;
compactionCount?: number;
manual_compaction_count?: number;
manualCompactionCount?: number;
automatic_compaction_count?: number;
automaticCompactionCount?: number;
saved_tokens?: number;
savedTokens?: number;
}

export interface RemoteUsageErrors {
total_errors?: number;
totalErrors?: number;
tool_errors?: number;
toolErrors?: number;
model_errors?: number;
modelErrors?: number;
}

export interface RemoteUsagePrivacy {
prompt_content_included?: boolean;
promptContentIncluded?: boolean;
tool_inputs_included?: boolean;
toolInputsIncluded?: boolean;
command_outputs_included?: boolean;
commandOutputsIncluded?: boolean;
file_contents_included?: boolean;
fileContentsIncluded?: boolean;
}

/**
* The `get_session_usage_report` value, as the desktop contract serializes it.
*
* The command answers with the report itself rather than a wrapper, so this is
* the `value` of the `host_invoke` result and not a response envelope.
*/
export interface RemoteSessionUsageReport {
schema_version?: number;
schemaVersion?: number;
report_id?: string;
reportId?: string;
session_id?: string;
sessionId?: string;
generated_at?: number;
generatedAt?: number;
workspace?: RemoteUsageWorkspace;
scope?: RemoteUsageScope;
coverage?: RemoteUsageCoverage;
time?: RemoteUsageTime;
tokens?: RemoteUsageTokens;
models?: RemoteUsageModelRow[];
tools?: RemoteUsageToolRow[];
files?: RemoteUsageFiles;
compression?: RemoteUsageCompression;
errors?: RemoteUsageErrors;
privacy?: RemoteUsagePrivacy;
}

export interface RemoteModelConfig {
id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,61 @@ export enum ConversationIntentType {
OpenFilePreview = 'open_file_preview',
DownloadFile = 'download_file',
BuildPlan = 'build_plan',
CommandPalette = 'command_palette',
Send = 'send',
VoiceInput = 'voice_input',
ChatInputChanged = 'chat_input_changed'
}

/**
* What one tap in the composer command row asks for.
*
* The row is a single funnel by construction: the two verbs keep their wire ids,
* and everything else is a local decision about the draft or a sheet. None of
* these carry a session — the active conversation is resolved once, where the
* command is sent.
*/
export enum CommandPaletteAction {
Compact = 'compact',
InitAgentsMd = 'init_agents_md',
ToggleGoal = 'toggle_goal',
OpenUsage = 'open_usage',
RetryUsage = 'retry_usage',
OpenSkills = 'open_skills',
RefreshCatalog = 'refresh_catalog',
SelectSkill = 'select_skill',
ClearSkill = 'clear_skill',
RetryCommand = 'retry_command'
}

export class CommandPaletteRequest {
readonly action: CommandPaletteAction;
/** The picked skill's key, for `SelectSkill`; empty otherwise. */
readonly skillId: string;
/** The verb a `RetryCommand` repeats, empty otherwise. */
readonly wireAction: string;
/**
* The picked skill's name, for `SelectSkill`; empty otherwise.
*
* It travels beside the key because the two are not the same string and the
* user sees only this one: the message carries `[$<name>]` while the row's
* identity, and every comparison the picker makes, stays the key.
*/
readonly skillLabel: string;

constructor(
action: CommandPaletteAction,
skillId: string = '',
wireAction: string = '',
skillLabel: string = ''
) {
this.action = action;
this.skillId = skillId;
this.wireAction = wireAction;
this.skillLabel = skillLabel;
}
}

export class PlanBuildRequest {
readonly path: string;
readonly name: string;
Expand Down Expand Up @@ -66,6 +116,7 @@ export class ConversationIntent {
readonly filePreviewRequest?: FilePreviewRequest;
readonly planBuildRequest?: PlanBuildRequest;
readonly copyScope: MessageCopyScope;
readonly commandPaletteRequest?: CommandPaletteRequest;
mailboxAction?: InteractionMailboxAction;

constructor(
Expand All @@ -76,7 +127,8 @@ export class ConversationIntent {
answers?: ConversationUiQuestionAnswer,
filePreviewRequest?: FilePreviewRequest,
planBuildRequest?: PlanBuildRequest,
copyScope: MessageCopyScope = MessageCopyScope.Plain
copyScope: MessageCopyScope = MessageCopyScope.Plain,
commandPaletteRequest?: CommandPaletteRequest
) {
this.type = type;
this.value = value;
Expand All @@ -86,6 +138,7 @@ export class ConversationIntent {
this.filePreviewRequest = filePreviewRequest;
this.planBuildRequest = planBuildRequest;
this.copyScope = copyScope;
this.commandPaletteRequest = commandPaletteRequest;
}
}

Expand Down Expand Up @@ -154,4 +207,19 @@ export class ConversationIntents {
new PlanBuildRequest(path, name)
);
}

/** One tap in the composer command row, as a typed request rather than a parameter list. */
static commandPalette(request: CommandPaletteRequest): ConversationIntent {
return new ConversationIntent(
ConversationIntentType.CommandPalette,
'',
'',
undefined,
undefined,
undefined,
undefined,
MessageCopyScope.Plain,
request
);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { InteractionMailboxAction } from '../../model/InteractionMailbox';
import { RemoteQuestionAnswerPayload, RemoteSession } from '../../model/RemoteModels';
import { AppRoute, AppRouteContract } from '../navigation/AppRouteContract';
import { ConversationIntent, ConversationIntentType, MessageCopyScope } from './ConversationIntent';
import { ConversationIntent, ConversationIntentType, CommandPaletteRequest, MessageCopyScope } from './ConversationIntent';
import { toRemoteQuestionAnswer } from '../state/ConversationUiModels';
import { FilePreviewRequest } from '../../model/FilePreviewTarget';

Expand All @@ -26,6 +26,7 @@ export interface ConversationIntentDispatcherHooks {
readonly openFilePreview: (route: AppRoute, request: FilePreviewRequest) => void;
readonly downloadFile: (path: string) => void;
readonly buildPlan: (path: string, name: string) => Promise<void>;
readonly commandPalette: (request: CommandPaletteRequest) => Promise<void>;
readonly send: () => Promise<void>;
readonly voiceInput: () => Promise<void>;
readonly inputChanged: (route: AppRoute, value: string) => void;
Expand Down Expand Up @@ -73,6 +74,13 @@ export class ConversationIntentDispatcher {
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;
case ConversationIntentType.CommandPalette:
// Command palette verbs ride the same remote conversation as everything
// else, so a general-chat route must not reach the remote host with one.
if (route === AppRoute.RemoteChat && intent.commandPaletteRequest) {
void this.hooks.commandPalette(intent.commandPaletteRequest);
}
return;
}
}

Expand Down
Loading
Loading