From e355a351dbe3c4cb8c9da01855f77789f9392756 Mon Sep 17 00:00:00 2001 From: Marqle Date: Fri, 24 Apr 2026 10:57:12 +0800 Subject: [PATCH 01/31] vcoder base upload --- app/oh/Vcoder/.gitignore | 12 ++ app/oh/Vcoder/AppScope/app.json5 | 11 + .../resources/base/element/string.json | 8 + .../resources/base/media/background.png | Bin 0 -> 91942 bytes .../resources/base/media/foreground.png | Bin 0 -> 15325 bytes .../resources/base/media/layered_image.json | 7 + app/oh/Vcoder/build-profile.json5 | 56 ++++++ app/oh/Vcoder/code-linter.json5 | 32 +++ app/oh/Vcoder/entry/.gitignore | 6 + app/oh/Vcoder/entry/build-profile.json5 | 38 ++++ app/oh/Vcoder/entry/hvigorfile.ts | 6 + app/oh/Vcoder/entry/obfuscation-rules.txt | 23 +++ app/oh/Vcoder/entry/oh-package-lock.json5 | 34 ++++ app/oh/Vcoder/entry/oh-package.json5 | 14 ++ .../Vcoder/entry/src/main/cpp/CMakeLists.txt | 15 ++ .../Vcoder/entry/src/main/cpp/napi_init.cpp | 53 +++++ .../types/libbitfun_desktop_lib/Index.d.ts | 2 + .../libbitfun_desktop_lib/oh-package.json5 | 6 + .../src/main/cpp/types/libentry/Index.d.ts | 1 + .../main/cpp/types/libentry/oh-package.json5 | 6 + .../main/ets/entryability/EntryAbility.ets | 190 ++++++++++++++++++ .../entrybackupability/EntryBackupAbility.ets | 16 ++ .../Vcoder/entry/src/main/ets/pages/Index.ets | 23 +++ .../main/ets/utils/CommonEventListener.ets | 43 ++++ .../entry/src/main/ets/utils/CommonUtils.ets | 36 ++++ .../entry/src/main/ets/utils/DevecoStart.ets | 78 +++++++ .../entry/src/main/ets/utils/Result.ets | 20 ++ app/oh/Vcoder/entry/src/main/module.json5 | 152 ++++++++++++++ .../main/resources/base/element/color.json | 8 + .../main/resources/base/element/float.json | 8 + .../main/resources/base/element/string.json | 16 ++ .../main/resources/base/media/background.png | Bin 0 -> 91942 bytes .../main/resources/base/media/foreground.png | Bin 0 -> 8805 bytes .../resources/base/media/layered_image.json | 7 + .../main/resources/base/media/startIcon.png | Bin 0 -> 20093 bytes .../resources/base/profile/backup_config.json | 3 + .../resources/base/profile/main_pages.json | 5 + .../main/resources/dark/element/color.json | 8 + .../Vcoder/entry/src/mock/mock-config.json5 | 2 + .../src/ohosTest/ets/test/Ability.test.ets | 35 ++++ .../entry/src/ohosTest/ets/test/List.test.ets | 5 + app/oh/Vcoder/entry/src/ohosTest/module.json5 | 12 ++ app/oh/Vcoder/entry/src/test/List.test.ets | 5 + .../Vcoder/entry/src/test/LocalUnit.test.ets | 33 +++ app/oh/Vcoder/hvigor/hvigor-config.json5 | 23 +++ app/oh/Vcoder/hvigorfile.ts | 6 + app/oh/Vcoder/oh-package-lock.json5 | 28 +++ app/oh/Vcoder/oh-package.json5 | 10 + 48 files changed, 1102 insertions(+) create mode 100644 app/oh/Vcoder/.gitignore create mode 100644 app/oh/Vcoder/AppScope/app.json5 create mode 100644 app/oh/Vcoder/AppScope/resources/base/element/string.json create mode 100644 app/oh/Vcoder/AppScope/resources/base/media/background.png create mode 100644 app/oh/Vcoder/AppScope/resources/base/media/foreground.png create mode 100644 app/oh/Vcoder/AppScope/resources/base/media/layered_image.json create mode 100644 app/oh/Vcoder/build-profile.json5 create mode 100644 app/oh/Vcoder/code-linter.json5 create mode 100644 app/oh/Vcoder/entry/.gitignore create mode 100644 app/oh/Vcoder/entry/build-profile.json5 create mode 100644 app/oh/Vcoder/entry/hvigorfile.ts create mode 100644 app/oh/Vcoder/entry/obfuscation-rules.txt create mode 100644 app/oh/Vcoder/entry/oh-package-lock.json5 create mode 100644 app/oh/Vcoder/entry/oh-package.json5 create mode 100644 app/oh/Vcoder/entry/src/main/cpp/CMakeLists.txt create mode 100644 app/oh/Vcoder/entry/src/main/cpp/napi_init.cpp create mode 100644 app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts create mode 100644 app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 create mode 100644 app/oh/Vcoder/entry/src/main/cpp/types/libentry/Index.d.ts create mode 100644 app/oh/Vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 create mode 100644 app/oh/Vcoder/entry/src/main/ets/entryability/EntryAbility.ets create mode 100644 app/oh/Vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets create mode 100644 app/oh/Vcoder/entry/src/main/ets/pages/Index.ets create mode 100644 app/oh/Vcoder/entry/src/main/ets/utils/CommonEventListener.ets create mode 100644 app/oh/Vcoder/entry/src/main/ets/utils/CommonUtils.ets create mode 100644 app/oh/Vcoder/entry/src/main/ets/utils/DevecoStart.ets create mode 100644 app/oh/Vcoder/entry/src/main/ets/utils/Result.ets create mode 100644 app/oh/Vcoder/entry/src/main/module.json5 create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/element/color.json create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/element/float.json create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/element/string.json create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/media/background.png create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/media/foreground.png create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/media/layered_image.json create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/media/startIcon.png create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/profile/backup_config.json create mode 100644 app/oh/Vcoder/entry/src/main/resources/base/profile/main_pages.json create mode 100644 app/oh/Vcoder/entry/src/main/resources/dark/element/color.json create mode 100644 app/oh/Vcoder/entry/src/mock/mock-config.json5 create mode 100644 app/oh/Vcoder/entry/src/ohosTest/ets/test/Ability.test.ets create mode 100644 app/oh/Vcoder/entry/src/ohosTest/ets/test/List.test.ets create mode 100644 app/oh/Vcoder/entry/src/ohosTest/module.json5 create mode 100644 app/oh/Vcoder/entry/src/test/List.test.ets create mode 100644 app/oh/Vcoder/entry/src/test/LocalUnit.test.ets create mode 100644 app/oh/Vcoder/hvigor/hvigor-config.json5 create mode 100644 app/oh/Vcoder/hvigorfile.ts create mode 100644 app/oh/Vcoder/oh-package-lock.json5 create mode 100644 app/oh/Vcoder/oh-package.json5 diff --git a/app/oh/Vcoder/.gitignore b/app/oh/Vcoder/.gitignore new file mode 100644 index 000000000..d2ff20141 --- /dev/null +++ b/app/oh/Vcoder/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/app/oh/Vcoder/AppScope/app.json5 b/app/oh/Vcoder/AppScope/app.json5 new file mode 100644 index 000000000..61c2b0373 --- /dev/null +++ b/app/oh/Vcoder/AppScope/app.json5 @@ -0,0 +1,11 @@ +{ + "app": { + "bundleName": "com.huawei.vcoder", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "buildVersion": "1", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/app/oh/Vcoder/AppScope/resources/base/element/string.json b/app/oh/Vcoder/AppScope/resources/base/element/string.json new file mode 100644 index 000000000..7162e0a96 --- /dev/null +++ b/app/oh/Vcoder/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "Vcoder" + } + ] +} diff --git a/app/oh/Vcoder/AppScope/resources/base/media/background.png b/app/oh/Vcoder/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f GIT binary patch literal 91942 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 M;JyCV_snDe2la%mzyJUM literal 0 HcmV?d00001 diff --git a/app/oh/Vcoder/AppScope/resources/base/media/layered_image.json b/app/oh/Vcoder/AppScope/resources/base/media/layered_image.json new file mode 100644 index 000000000..fb4992044 --- /dev/null +++ b/app/oh/Vcoder/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/build-profile.json5 b/app/oh/Vcoder/build-profile.json5 new file mode 100644 index 000000000..c792291de --- /dev/null +++ b/app/oh/Vcoder/build-profile.json5 @@ -0,0 +1,56 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\lijiale\\.ohos\\config\\default_Vcoder_imQPBwlDhZKHpu-LkgPseVnUO1DIbVYZGHwovJ97cvY=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B63A5404F339099984BDD616073E604D447C289B47D5DA41028E43A2E5D4DF2BE67AA24F4CD3464", + "profile": "C:\\Users\\lijiale\\.ohos\\config\\default_Vcoder_imQPBwlDhZKHpu-LkgPseVnUO1DIbVYZGHwovJ97cvY=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\lijiale\\.ohos\\config\\default_Vcoder_imQPBwlDhZKHpu-LkgPseVnUO1DIbVYZGHwovJ97cvY=.p12", + "storePassword": "0000001B8D25281252401BEC116357BEDCA9ABEF740814063184A05219DA4399AB405785E0DFC726A24DA1" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "6.1.0(23)", + "compatibleSdkVersion": "6.1.0(23)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/app/oh/Vcoder/code-linter.json5 b/app/oh/Vcoder/code-linter.json5 new file mode 100644 index 000000000..073990fa4 --- /dev/null +++ b/app/oh/Vcoder/code-linter.json5 @@ -0,0 +1,32 @@ +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@security/no-unsafe-aes": "error", + "@security/no-unsafe-hash": "error", + "@security/no-unsafe-mac": "warn", + "@security/no-unsafe-dh": "error", + "@security/no-unsafe-dsa": "error", + "@security/no-unsafe-ecdsa": "error", + "@security/no-unsafe-rsa-encrypt": "error", + "@security/no-unsafe-rsa-sign": "error", + "@security/no-unsafe-rsa-key": "error", + "@security/no-unsafe-dsa-key": "error", + "@security/no-unsafe-dh-key": "error", + "@security/no-unsafe-3des": "error" + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/.gitignore b/app/oh/Vcoder/entry/.gitignore new file mode 100644 index 000000000..e2713a277 --- /dev/null +++ b/app/oh/Vcoder/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/app/oh/Vcoder/entry/build-profile.json5 b/app/oh/Vcoder/entry/build-profile.json5 new file mode 100644 index 000000000..0d5517e3e --- /dev/null +++ b/app/oh/Vcoder/entry/build-profile.json5 @@ -0,0 +1,38 @@ +{ + "apiType": "stageMode", + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "" + }, + "resOptions": { + "copyCodeResource": { + "enable": false + } + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/hvigorfile.ts b/app/oh/Vcoder/entry/hvigorfile.ts new file mode 100644 index 000000000..b0e3a1ab9 --- /dev/null +++ b/app/oh/Vcoder/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/obfuscation-rules.txt b/app/oh/Vcoder/entry/obfuscation-rules.txt new file mode 100644 index 000000000..1e7e54e15 --- /dev/null +++ b/app/oh/Vcoder/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/app/oh/Vcoder/entry/oh-package-lock.json5 b/app/oh/Vcoder/entry/oh-package-lock.json5 new file mode 100644 index 000000000..d09758325 --- /dev/null +++ b/app/oh/Vcoder/entry/oh-package-lock.json5 @@ -0,0 +1,34 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos-rs/ability@0.4.0-beta.0": "@ohos-rs/ability@0.4.0-beta.0", + "libbitfun_desktop_lib.so@src/main/cpp/types/libbitfun_desktop_lib": "libbitfun_desktop_lib.so@src/main/cpp/types/libbitfun_desktop_lib", + "libentry.so@src/main/cpp/types/libentry": "libentry.so@src/main/cpp/types/libentry" + }, + "packages": { + "@ohos-rs/ability@0.4.0-beta.0": { + "name": "@ohos-rs/ability", + "version": "0.4.0-beta.0", + "integrity": "sha512-3jXF0SzSqdyIEcWZy+2i/LWueVEFuLB9J3hYDiNDrL6guTMDqojMy5o9svD6pHEpfjnU+T7058bRTjGD2+iohA==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos-rs/ability/-/ability-0.4.0-beta.0.har", + "registryType": "ohpm" + }, + "libbitfun_desktop_lib.so@src/main/cpp/types/libbitfun_desktop_lib": { + "name": "libbitfun_desktop_lib.so", + "version": "1.0.0", + "resolved": "src/main/cpp/types/libbitfun_desktop_lib", + "registryType": "local" + }, + "libentry.so@src/main/cpp/types/libentry": { + "name": "libentry.so", + "version": "1.0.0", + "resolved": "src/main/cpp/types/libentry", + "registryType": "local" + } + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/oh-package.json5 b/app/oh/Vcoder/entry/oh-package.json5 new file mode 100644 index 000000000..3fe6a279b --- /dev/null +++ b/app/oh/Vcoder/entry/oh-package.json5 @@ -0,0 +1,14 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "libbitfun_desktop_lib.so": "file:./src/main/cpp/types/libbitfun_desktop_lib", + "libentry.so": "file:./src/main/cpp/types/libentry", + "@ohos-rs/ability": "0.4.0-beta.0" + } +} + diff --git a/app/oh/Vcoder/entry/src/main/cpp/CMakeLists.txt b/app/oh/Vcoder/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..cadd21eef --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,15 @@ +# the minimum version of CMake. +cmake_minimum_required(VERSION 3.5.0) +project(MyApplication6) + +set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +if(DEFINED PACKAGE_FIND_FILE) + include(${PACKAGE_FIND_FILE}) +endif() + +include_directories(${NATIVERENDER_ROOT_PATH} + ${NATIVERENDER_ROOT_PATH}/include) + +add_library(entry SHARED napi_init.cpp) +target_link_libraries(entry PUBLIC libace_napi.z.so) \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/cpp/napi_init.cpp b/app/oh/Vcoder/entry/src/main/cpp/napi_init.cpp new file mode 100644 index 000000000..987bd48bd --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/cpp/napi_init.cpp @@ -0,0 +1,53 @@ +#include "napi/native_api.h" + +static napi_value Add(napi_env env, napi_callback_info info) +{ + size_t argc = 2; + napi_value args[2] = {nullptr}; + + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + napi_valuetype valuetype0; + napi_typeof(env, args[0], &valuetype0); + + napi_valuetype valuetype1; + napi_typeof(env, args[1], &valuetype1); + + double value0; + napi_get_value_double(env, args[0], &value0); + + double value1; + napi_get_value_double(env, args[1], &value1); + + napi_value sum; + napi_create_double(env, value0 + value1, &sum); + + return sum; + +} + +EXTERN_C_START +static napi_value Init(napi_env env, napi_value exports) +{ + napi_property_descriptor desc[] = { + { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr } + }; + napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); + return exports; +} +EXTERN_C_END + +static napi_module demoModule = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = nullptr, + .nm_register_func = Init, + .nm_modname = "entry", + .nm_priv = ((void*)0), + .reserved = { 0 }, +}; + +extern "C" __attribute__((constructor)) void RegisterEntryModule(void) +{ + napi_module_register(&demoModule); +} diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts b/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts new file mode 100644 index 000000000..8c9512b8f --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts @@ -0,0 +1,2 @@ +export declare function registerArktsFunction(funcName: string, callback: ((err: Error | null, arg: string) => Promise)): void; +export declare function setBuildResult(msg: string): void; \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 b/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 new file mode 100644 index 000000000..8b10ca321 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 @@ -0,0 +1,6 @@ +{ + "name": "libbitfun_desktop_lib.so", + "types": "./Index.d.ts", + "version": "1.0.0", + "description": "Please describe the basic information." +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libentry/Index.d.ts b/app/oh/Vcoder/entry/src/main/cpp/types/libentry/Index.d.ts new file mode 100644 index 000000000..e44f3615a --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/cpp/types/libentry/Index.d.ts @@ -0,0 +1 @@ +export const add: (a: number, b: number) => number; \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 b/app/oh/Vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 new file mode 100644 index 000000000..ea410725a --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 @@ -0,0 +1,6 @@ +{ + "name": "libentry.so", + "types": "./Index.d.ts", + "version": "1.0.0", + "description": "Please describe the basic information." +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/app/oh/Vcoder/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 000000000..29649656c --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,190 @@ +import { + abilityAccessCtrl, AbilityConstant, common, Permissions, Want +} from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import { RustAbility } from '@ohos-rs/ability'; +import { CommonEventListener } from '../utils/CommonEventListener'; +import { harmonyShare, systemShare } from '@kit.ShareKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { uniformTypeDescriptor } from '@kit.ArkData'; +import { calendarManager } from '@kit.CalendarKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import RustModule from 'libbitfun_desktop_lib.so'; +import { CommonUtils } from '../utils/CommonUtils'; +import { runDeveco } from '../utils/DevecoStart'; + + +const DOMAIN = 0x0000; + +export default class EntryAbility extends RustAbility { + public moduleName: string = "bitfun_desktop_lib"; + public defaultPage: boolean = true; + public commonEventListener: CommonEventListener | undefined = undefined; + + async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise { + super.onCreate(want, launchParam); + this.commonEventListener = new CommonEventListener(); + this.aboutToAppear(); + } + + onDestroy(): void { + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): Promise { + AppGlobal.calendarMgr = calendarManager.getCalendarManager(this.context); + AppGlobal.mContext = this.context; + const permissions: Permissions[] = ['ohos.permission.READ_CALENDAR', 'ohos.permission.WRITE_CALENDAR']; + const DOMAIN_NUMBER: number = 0xFF00; + const TAG: string = '[BitfunUIAbilityComponents]'; + + let atManager = abilityAccessCtrl.createAtManager(); + AppGlobal.calendarMgr = calendarManager.getCalendarManager(this.context); + + atManager.requestPermissionsFromUser(this.context, permissions).then(() => { + AppGlobal.calendarMgr = calendarManager.getCalendarManager(this.context); + }).catch((error: BusinessError) => { + hilog.info(DOMAIN_NUMBER, TAG, 'get permissions error: ' + error); + }); + RustModule.registerArktsFunction('open_dialog_file', async (err: Error, arg: string): Promise => { + let res = await CommonUtils.open_file_dialog(); + return res; + }); + RustModule.registerArktsFunction('handle_min_window', async (err: Error, arg: string): Promise => { + windowStage.getMainWindowSync().minimize(); + return ''; + }); + RustModule.registerArktsFunction('handle_max_window', async (err: Error, arg: string): Promise => { + windowStage.getMainWindowSync().maximize(); + return ''; + }); + RustModule.registerArktsFunction('handle_restore_window', async (err: Error, arg: string): Promise => { + windowStage.getMainWindowSync().recover(); + return ''; + }); + RustModule.registerArktsFunction('window_is_maximized', async (err: Error, arg: string): Promise => { + let state = windowStage.getMainWindowSync().getWindowStatus(); + if (state == window.WindowStatusType.FULL_SCREEN || state == window.WindowStatusType.MAXIMIZE) { + return 'true'; + } else { + return 'false'; + } + }); + RustModule.registerArktsFunction('window_is_minimized', async (err: Error, arg: string): Promise => { + let state = windowStage.getMainWindowSync().getWindowStatus(); + if (state == window.WindowStatusType.MINIMIZE) { + return 'true'; + } else { + return 'false'; + } + }); + RustModule.registerArktsFunction('close_window', async (err: Error, arg: string): Promise => { + this.context.terminateSelf(() => { + hilog.info(DOMAIN_NUMBER, TAG, 'terminateSelf success'); + }); + return ''; + }); + RustModule.registerArktsFunction('window_start_dragging', async (err: Error, arg: string): Promise => { + windowStage.getMainWindowSync().startMoving(); + return ''; + }); + RustModule.registerArktsFunction('call_calendar', async (err: Error, arg: string): Promise => { + return await createMeetingEvent(arg); + }); + RustModule.registerArktsFunction('call_harmony_build', async (err: Error, arg: string): Promise => { + hilog.info(DOMAIN_NUMBER, TAG, 'register harmony build callback ' + arg); + runDeveco(this.context, arg); + return ''; + }); + RustModule.registerArktsFunction('harmony_create', async (err: Error, arg: string): Promise => { + await fileIo.copyDir('/storage/Users/currentUser/Documents/DevecoStudioProjects/MyApplication', + '/storage/Users/currentUser/Documents/files', 1).then(() => { + hilog.info(0x0000, 'vnext', 'copyDir success'); + }).catch((err: BusinessError) => { + hilog.info(0x0000, 'vnext', 'copyFile error: ' + JSON.stringify(err)); + }); + await fileIo.rename('/storage/Users/currentUser/Documents/files', + `/storage/Users/currentUser/Documents/DevecoStudioProjects/${arg.length == 0 ? 'MyApplication' : arg}`) + .then(() => { + hilog.info(0x0000, 'vnext', 'rename success'); + }).catch((err: BusinessError) => { + hilog.info(0x0000, 'vnext', 'rename error: ' + JSON.stringify(err)); + }); + return ''; + }); + return super.onWindowStageCreate(windowStage); + } + + aboutToAppear(): void { + console.info("aboutToAppear"); + harmonyShare.on('knockShare', this.sendOnlyCallback); + } + + private sendOnlyCallback = (sharableTable: harmonyShare.SharableTarget) => { + let filePath = "/data/storage/el2/base/files/dist/output.txt"; + fileIo.readText(filePath).then((content: string) => { + console.info("readText success:" + content); + let shareData: systemShare.SharedData = new systemShare.SharedData({ + utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, + content, + title: "Bitfun", + description: "Phone", + }); + sharableTable.share(shareData) + }).catch((err: BusinessError) => { + hilog.error(DOMAIN, 'vnext', 'sendOnlyCallback error:' + err); + }); + } +} + +interface AppGlobalType { + calendarMgr: calendarManager.CalendarManager | null, + mContext: common.UIAbilityContext | null +} + +export const AppGlobal: AppGlobalType = { + calendarMgr: null, + mContext: null +} + +export async function createMeetingEvent(input: string): Promise { + interface CalendarInfo { + title: string, + description: string, + startTime: string, + endTime: string + } + + const info: CalendarInfo = JSON.parse(input) + const calendarAccount: calendarManager.CalendarAccount = { + name: info.title, + type: calendarManager.CalendarType.LOCAL, + displayName: 'vcoder', + }; + const startTime = new Date(info.startTime).valueOf(); + + const time = info.endTime == '' ? startTime : info.endTime; + const endTime = new Date(time).valueOf(); + + const event: calendarManager.Event = { + type: calendarManager.EventType.NORMAL, + title: info.title, + startTime: startTime, + endTime: endTime, + isAllDay: false, + description: info.description + }; + + try { + let data: calendarManager.Calendar | undefined = await AppGlobal.calendarMgr?.createCalendar(calendarAccount); + if (!data || data === null) { + hilog.warn(0x0000, 'vnext', 'Failed to create calendar, data is null'); + return "" + } + let id = await data.addEvent(event); + return "succeed in creating calendar and event " + id; + } catch (error) { + return "Failed to create calendar or event. Code: " + error; + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/app/oh/Vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 000000000..8e4de9928 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,16 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/ets/pages/Index.ets b/app/oh/Vcoder/entry/src/main/ets/pages/Index.ets new file mode 100644 index 000000000..8e2d24ad4 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,23 @@ +@Entry +@Component +struct Index { + @State message: string = 'Hello World'; + + build() { + RelativeContainer() { + Text(this.message) + .id('HelloWorld') + .fontSize($r('app.float.page_text_font_size')) + .fontWeight(FontWeight.Bold) + .alignRules({ + center: { anchor: '__container__', align: VerticalAlign.Center }, + middle: { anchor: '__container__', align: HorizontalAlign.Center } + }) + .onClick(() => { + this.message = 'Welcome'; + }) + } + .height('100%') + .width('100%') + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/CommonEventListener.ets b/app/oh/Vcoder/entry/src/main/ets/utils/CommonEventListener.ets new file mode 100644 index 000000000..302482185 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/ets/utils/CommonEventListener.ets @@ -0,0 +1,43 @@ +import commonEventManager from "@ohos.commonEventManager"; +import { hilog } from "@kit.PerformanceAnalysisKit"; +import Base from '@ohos.base'; +import NativeModule from 'libbitfun_desktop_lib.so'; + +export class CommonEventListener { + protected subscriber: commonEventManager.CommonEventSubscriber | null = null; + protected subscriberInfo: commonEventManager.CommonEventSubscribeInfo = { + events: ["DEVECO_BUILD_END"] + }; + + constructor() { + hilog.info(0x0000, 'vnext', 'create CommonEventListener'); + this.subscriber = commonEventManager.createSubscriberSync(this.subscriberInfo); + if (this.subscriber !== null) { + commonEventManager.subscribe(this.subscriber, (err: Base.BusinessError, data: commonEventManager.CommonEventData) => { + if (err) { + hilog.error(0x0000, 'vnext', 'receive message error: ' + err); + return; + } + + hilog.info(0x0000, 'vnext', 'receive data ' + JSON.stringify(data)); + switch (data.event) { + case "DEVECO_BUILD_END": + let msg = getMessageParam(data, "msg"); + hilog.info(0x0000, 'vnext', 'receive message DEVECO_BUILD_END: ' + msg); + NativeModule.setBuildResult(msg); + } + }) + } else { + hilog.error(0x0000, 'vnext', 'subscriber is null!'); + } + } +} + +export function getMessageParam(data: commonEventManager.CommonEventData, key: string): T | undefined { + let value: T | undefined = undefined; + let param = data.parameters; + if (param) { + value = param[key]; + } + return value; +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/CommonUtils.ets b/app/oh/Vcoder/entry/src/main/ets/utils/CommonUtils.ets new file mode 100644 index 000000000..d93a097ca --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/ets/utils/CommonUtils.ets @@ -0,0 +1,36 @@ +import { bundleManager } from "@kit.AbilityKit"; +import hilog from "@ohos.hilog"; +import filePicker from '@ohos.file.picker'; +import fileUri from "@ohos.file.fileuri"; +import { Err, Ok, Result } from "./Result"; + +export class CommonUtils { + static getBundleName(): string { + let bundleName = ""; + try { + bundleName = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT).name; + } catch (e) { + hilog.warn(0x0000, 'vnext', 'getBundleName failed' + e); + } + return bundleName; + } + + static async open_file_dialog(): Promise { + try { + hilog.info(0x0000, 'vnext', 'open_file_dialog'); + let documentOptions = new filePicker.DocumentSelectOptions; + documentOptions.defaultFilePathUri = 'file://docs/storage/Users/currentUser'; + documentOptions.selectMode = filePicker.DocumentSelectMode.MIXED; + documentOptions.maxSelectNumber = 1; + let documentPicker = new filePicker.DocumentViewPicker(); + hilog.info(0x0000, 'vnext', 'open filePicker success'); + let select = await documentPicker.select(documentOptions); + let fileUrl = new fileUri.FileUri(select.pop() as string); + let tmp = fileUrl.path.replace('/storage/Users/currentUser/appdata', '/data/storage'); + let uri = tmp.replace(CommonUtils.getBundleName(), 'base'); + return uri + } catch (e) { + return 'open file or folder failed'; + } + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/DevecoStart.ets b/app/oh/Vcoder/entry/src/main/ets/utils/DevecoStart.ets new file mode 100644 index 000000000..9b0701abe --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/ets/utils/DevecoStart.ets @@ -0,0 +1,78 @@ +import common from "@ohos.app.ability.common"; +import { JSON } from "@kit.ArkTS"; +import hilog from "@ohos.hilog"; +import Want from "@ohos.app.ability.Want"; +import { CommonUtils } from "./CommonUtils"; +import { fileIo } from "@kit.CoreFileKit"; + +let srcDirPathLocal: string = + '/storage/Users/currentUser/Documents/DevecoStudioProjects/MyApplication/build-profile.json5' + +export function runDeveco(context: common.UIAbilityContext, workspace: string): void { + let args = defaultStartArgs(); + args.workspace = workspace; + args.build_after_start = true; + startNewInstance(context, args) +} + +export function copySignFile(path: string): void { + let dstDirPathLocal: string = path + "/build-profile.json5"; + try { + fileIo.copyFile(srcDirPathLocal, dstDirPathLocal, 0).then(() => { + hilog.info(0x0000, 'vnext', 'copyFile success '); + }).catch((err: BusinessError) => { + hilog.info(0x0000, 'vnext', 'copyFile error: ' + JSON.stringify(err)); + }) + } catch (err) { + hilog.info(0x0000, 'vnext', 'copyFile error: ' + JSON.stringify(err)); + } +} + +export function startNewInstance(context: common.UIAbilityContext, args: StartArgs | undefined): void { + hilog.info(0x0000, 'vnext', 'startNewInstance ' + JSON.stringify(args)); + const want: Want = { + bundleName: 'com.huawei.devecostudio', + abilityName: 'EntryAbility', + parameters: { + 'startParameters': JSON.stringify(args), + } + }; + context.startAbility(want).then(() => { + hilog.info(0x0000, 'vnext', 'startAbility success '); + }).catch((error: BusinessError) => { + hilog.warn(0x0000, 'vnext', `startAbility failed: code: ${error.code}, msg: ${error.message} `); + }); + +} + +export interface StartArgs { + workspace: string, + opened_files: string, + window_state: WindowState, + x: number | undefined, + y: number | undefined, + width: number | undefined, + height: number | undefined, + restart: boolean, + build_after_start: boolean +} + +export enum WindowState { + Restored = "Restored", + Maximized = "Maximized", + Minimized = "Minimized" +} + +export function defaultStartArgs(): StartArgs { + return { + workspace: "", + opened_files: "", + window_state: WindowState.Restored, + x: undefined, + y: undefined, + width: undefined, + height: undefined, + restart: false, + build_after_start: false + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/Result.ets b/app/oh/Vcoder/entry/src/main/ets/utils/Result.ets new file mode 100644 index 000000000..eb405f2e9 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/ets/utils/Result.ets @@ -0,0 +1,20 @@ +export interface ResultInner { + Ok?: T; + Err?: E; +} + +export class Result { + Ok?: T; + Err?: E; + constructor(result: ResultInner) { + this.Ok = result.Ok; + this.Err = result.Err; + } +} +export function Ok(ok?: T): Result { + return new Result({ Ok: ok }); +} + +export function Err(err?: E): Result { + return new Result({ Err: err }); +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/module.json5 b/app/oh/Vcoder/entry/src/main/module.json5 new file mode 100644 index 000000000..dd22d6d8a --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/module.json5 @@ -0,0 +1,152 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "hnpPackages": [ + { + "package": "node.hnp", + "type": "private" + }, + { + "package": "git.hnp", + "type": "private" + } + ], + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "ohos.want.action.home" + ] + } + ] + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.PREPARE_APP_TERMINATE" + }, + { + "name": "ohos.permission.INTERNET" + }, +// { +// "name": "ohos.permission.GET_ALL_PROCESSES" +// }, + { + "name": "ohos.permission.READ_WRITE_USER_FILE" + }, +// { +// "name": "ohos.permission.SECURE_PASTE" +// }, + { + "name": "ohos.permission.CUSTOM_SANDBOX" + }, +// { +// "name": "ohos.permission.MANAGE_USER_IDM" +// }, +// { +// "name": "ohos.permission.ACCESS_PIN_AUTH" +// }, +// { +// "name": "ohos.permission.ACCESS_USER_AUTH_INTERNAL" +// }, +// { +// "name": "ohos.permission.DUMP" +// }, +// { +// "name": "ohos.permission.READ_DIAGNOSTIC_LOGS" +// }, +// { +// "name": "ohos.permission.READ_DFX_SYSEVENT" +// }, +// { +// "name": "ohos.permission.GET_BUNDLE_INFO_PRIVILEGED" +// }, + { + "name": "ohos.permission.READ_CALENDAR", + "reason": "$string:module_desc", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.WRITE_CALENDAR", + "reason": "$string:module_desc", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.READ_WRITE_DESKTOP_DIRECTORY", + "reason": "$string:module_desc", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY", + "reason": "$string:module_desc", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY", + "reason": "$string:module_desc", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "always" + } + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ] + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/resources/base/element/color.json b/app/oh/Vcoder/entry/src/main/resources/base/element/color.json new file mode 100644 index 000000000..3c712962d --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/resources/base/element/float.json b/app/oh/Vcoder/entry/src/main/resources/base/element/float.json new file mode 100644 index 000000000..33ea22304 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/app/oh/Vcoder/entry/src/main/resources/base/element/string.json b/app/oh/Vcoder/entry/src/main/resources/base/element/string.json new file mode 100644 index 000000000..f94595515 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/resources/base/media/background.png b/app/oh/Vcoder/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f GIT binary patch literal 91942 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

c)ijgxDm}8EsRXFt3h@+Ufr7@DN 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 literal 0 HcmV?d00001 diff --git a/app/oh/Vcoder/entry/src/main/resources/base/profile/backup_config.json b/app/oh/Vcoder/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 000000000..78f40ae7c --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/main/resources/base/profile/main_pages.json b/app/oh/Vcoder/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 000000000..1898d94f5 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/app/oh/Vcoder/entry/src/main/resources/dark/element/color.json b/app/oh/Vcoder/entry/src/main/resources/dark/element/color.json new file mode 100644 index 000000000..79b11c274 --- /dev/null +++ b/app/oh/Vcoder/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/mock/mock-config.json5 b/app/oh/Vcoder/entry/src/mock/mock-config.json5 new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/app/oh/Vcoder/entry/src/mock/mock-config.json5 @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/ohosTest/ets/test/Ability.test.ets b/app/oh/Vcoder/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 000000000..85c78f675 --- /dev/null +++ b/app/oh/Vcoder/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/ohosTest/ets/test/List.test.ets b/app/oh/Vcoder/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 000000000..794c7dc4e --- /dev/null +++ b/app/oh/Vcoder/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/ohosTest/module.json5 b/app/oh/Vcoder/entry/src/ohosTest/module.json5 new file mode 100644 index 000000000..b94766632 --- /dev/null +++ b/app/oh/Vcoder/entry/src/ohosTest/module.json5 @@ -0,0 +1,12 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/app/oh/Vcoder/entry/src/test/List.test.ets b/app/oh/Vcoder/entry/src/test/List.test.ets new file mode 100644 index 000000000..bb5b5c373 --- /dev/null +++ b/app/oh/Vcoder/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/app/oh/Vcoder/entry/src/test/LocalUnit.test.ets b/app/oh/Vcoder/entry/src/test/LocalUnit.test.ets new file mode 100644 index 000000000..165fc1615 --- /dev/null +++ b/app/oh/Vcoder/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/app/oh/Vcoder/hvigor/hvigor-config.json5 b/app/oh/Vcoder/hvigor/hvigor-config.json5 new file mode 100644 index 000000000..20f88f34a --- /dev/null +++ b/app/oh/Vcoder/hvigor/hvigor-config.json5 @@ -0,0 +1,23 @@ +{ + "modelVersion": "6.1.0", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + // "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/app/oh/Vcoder/hvigorfile.ts b/app/oh/Vcoder/hvigorfile.ts new file mode 100644 index 000000000..47113e2e3 --- /dev/null +++ b/app/oh/Vcoder/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/app/oh/Vcoder/oh-package-lock.json5 b/app/oh/Vcoder/oh-package-lock.json5 new file mode 100644 index 000000000..c5a91ec3c --- /dev/null +++ b/app/oh/Vcoder/oh-package-lock.json5 @@ -0,0 +1,28 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.25": "@ohos/hypium@1.0.25" + }, + "packages": { + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.25": { + "name": "@ohos/hypium", + "version": "1.0.25", + "integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/app/oh/Vcoder/oh-package.json5 b/app/oh/Vcoder/oh-package.json5 new file mode 100644 index 000000000..41eb3e0c5 --- /dev/null +++ b/app/oh/Vcoder/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "6.1.0", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.25", + "@ohos/hamock": "1.0.0" + } +} From 5da4128cfb736a622c75c52d644d46f2c7c8db2c Mon Sep 17 00:00:00 2001 From: Marqle Date: Fri, 24 Apr 2026 15:07:05 +0800 Subject: [PATCH 02/31] rm hnp --- app/oh/Vcoder/entry/src/main/module.json5 | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/oh/Vcoder/entry/src/main/module.json5 b/app/oh/Vcoder/entry/src/main/module.json5 index dd22d6d8a..614326843 100644 --- a/app/oh/Vcoder/entry/src/main/module.json5 +++ b/app/oh/Vcoder/entry/src/main/module.json5 @@ -2,16 +2,6 @@ "module": { "name": "entry", "type": "entry", - "hnpPackages": [ - { - "package": "node.hnp", - "type": "private" - }, - { - "package": "git.hnp", - "type": "private" - } - ], "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": [ From ea7b3aef18dd70129daa9fa2e2bcdafeb8f331a1 Mon Sep 17 00:00:00 2001 From: Marqle Date: Sat, 25 Apr 2026 09:35:32 +0800 Subject: [PATCH 03/31] modify path_manager --- .../infrastructure/app_paths/path_manager.rs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/crates/core/src/infrastructure/app_paths/path_manager.rs b/src/crates/core/src/infrastructure/app_paths/path_manager.rs index d42770ab6..4e7e70f11 100644 --- a/src/crates/core/src/infrastructure/app_paths/path_manager.rs +++ b/src/crates/core/src/infrastructure/app_paths/path_manager.rs @@ -32,6 +32,7 @@ pub enum StorageLevel { pub struct PathManager { /// User config root directory user_root: PathBuf, + home_dir: PathBuf, /// Optional override for the BitFun home directory, used by tests to avoid /// touching the real user home. bitfun_home_override: Option, @@ -43,9 +44,11 @@ impl PathManager { /// Create a new path manager pub fn new() -> BitFunResult { let user_root = Self::get_user_config_root()?; + let home_dir = Self::get_home_dir()?; Ok(Self { user_root, + home_dir, bitfun_home_override: None, project_runtime_slug_cache: Arc::new(Mutex::new(HashMap::new())), }) @@ -57,20 +60,20 @@ impl PathManager { /// - macOS: ~/Library/Application Support/BitFun/ /// - Linux: ~/.config/bitfun/ fn get_user_config_root() -> BitFunResult { - let config_dir = dirs::config_dir() - .ok_or_else(|| BitFunError::config("Failed to get config directory".to_string()))?; + Ok(PathBuf::from("/data/storage/el2/base/files/bitfun")) + } + + fn get_home_dir() -> BitFunResult { + Ok(PathBuf::from("/data/storage/el2/base/files/home_dir/.bitfun")) + } - Ok(config_dir.join("bitfun")) + pub fn home_dir(&self) -> PathBuf { + self.home_dir.clone() } /// Get assistant home root directory: ~/.bitfun/ pub fn bitfun_home_dir(&self) -> PathBuf { - if let Some(path) = &self.bitfun_home_override { - return path.clone(); - } - dirs::home_dir() - .unwrap_or_else(|| self.user_root.clone()) - .join(".bitfun") + self.home_dir() } /// Get the legacy assistant workspace base directory: ~/.bitfun/ @@ -197,6 +200,8 @@ impl PathManager { .join("Application Support") .join("BitFun") .join("skills") + } else if cfg!(target_env = "ohos") { + self.user_root.join("skills") } else { dirs::data_local_dir() .unwrap_or_else(|| PathBuf::from("/tmp")) @@ -449,6 +454,7 @@ impl Default for PathManager { ); Self { user_root: std::env::temp_dir().join("bitfun"), + home_dir: Self::get_home_dir().unwrap_or_default(), bitfun_home_override: None, project_runtime_slug_cache: Arc::new(Mutex::new(HashMap::new())), } @@ -466,6 +472,7 @@ impl PathManager { .unwrap_or_else(|| user_root.clone()); Self { user_root, + home_dir: Self::get_home_dir().unwrap_or_default(), bitfun_home_override: Some(base.join("home").join(".bitfun")), project_runtime_slug_cache: Arc::new(Mutex::new(HashMap::new())), } From ca8749cf48d3d5d57df65edca672b7065dd58af2 Mon Sep 17 00:00:00 2001 From: Marqle Date: Sat, 25 Apr 2026 14:55:12 +0800 Subject: [PATCH 04/31] adapt ohos --- Cargo.toml | 20 +- src/apps/desktop/Cargo.toml | 10 +- .../desktop/capabilities/browser-webview.json | 13 - src/apps/desktop/capabilities/default.json | 215 +- src/apps/desktop/src/api/browser_api.rs | 20 +- .../desktop/src/api/remote_connect_api.rs | 2 +- src/apps/desktop/src/api/system_api.rs | 8 +- src/apps/desktop/src/api/terminal_api.rs | 2 +- .../desktop/src/computer_use/desktop_host.rs | 2569 +---------------- src/apps/desktop/src/computer_use/mod.rs | 5 +- .../desktop/src/computer_use/screen_ocr.rs | 2 +- src/apps/desktop/src/lib.rs | 87 +- src/apps/desktop/src/logging.rs | 59 +- src/apps/desktop/src/main.rs | 12 +- src/apps/desktop/src/theme.rs | 26 +- src/apps/desktop/tauri.conf.json | 75 +- src/crates/core/Cargo.toml | 1 - .../src/agentic/tools/computer_use_host.rs | 2 +- .../core/src/service/file_watch/service.rs | 2 +- .../core/src/service/remote_connect/device.rs | 6 +- .../src/service/terminal/src/pty/process.rs | 10 +- src/crates/webdriver/src/executor/mod.rs | 28 +- src/crates/webdriver/src/executor/window.rs | 28 - src/crates/webdriver/src/runtime/mod.rs | 21 +- .../src/server/handlers/navigation.rs | 16 +- .../webdriver/src/server/handlers/session.rs | 20 +- .../webdriver/src/server/handlers/window.rs | 9 - src/crates/webdriver/src/server/mod.rs | 6 +- 28 files changed, 289 insertions(+), 2985 deletions(-) delete mode 100644 src/apps/desktop/capabilities/browser-webview.json diff --git a/Cargo.toml b/Cargo.toml index 5f6785bb8..996080345 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ toml = "0.8" git2 = { version = "0.18", default-features = false, features = ["https", "vendored-libgit2"] } # Terminal -portable-pty = "0.8" +portable-pty = "0.9.0" vte = "0.15.0" # Grep (search) @@ -101,14 +101,16 @@ similar = "2.5" urlencoding = "2.1" # Tauri (desktop only) - tauri = { version = "2", features = ["unstable"] } + tauri = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony", features = [] } +tauri-plugin-dialog = "2" tauri-plugin-opener = "2" -tauri-plugin-dialog = "2.6" tauri-plugin-fs = "2" tauri-plugin-log = "2" tauri-plugin-autostart = "2" tauri-plugin-notification = "2" -tauri-build = { version = "2", features = [] } +tauri-build = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony", features = [] } +napi-ohos = { version = "1.1" } +napi-derive-ohos = { version = "1.1" } # Windows-specific dependencies win32job = "2.0" @@ -150,3 +152,13 @@ lto = false codegen-units = 16 strip = false incremental = true + +[patch.crates-io] +wry = { git = "https://github.com/richerfu/wry"} +tao = { git = "https://github.com/richerfu/tao", branch = "feat-ohos-webview"} +openharmony-ability = {git = "https://github.com/harmony-contrib/openharmony-ability.git"} +openharmony-ability-derive = {git = "https://github.com/harmony-contrib/openharmony-ability.git"} +tauri = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} +tauri-runtime = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} +tauri-macros = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} +tauri-runtime-wry = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} \ No newline at end of file diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index e42f69dbf..1acb485b9 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -29,8 +29,8 @@ tauri-plugin-opener = { workspace = true } tauri-plugin-dialog = { workspace = true } tauri-plugin-fs = { workspace = true } tauri-plugin-log = { workspace = true } -tauri-plugin-autostart = { workspace = true } -tauri-plugin-notification = { workspace = true } +napi-ohos = { workspace = true } +napi-derive-ohos = { workspace = true } # Inherited from workspace tokio = { workspace = true } @@ -41,7 +41,6 @@ log = { workspace = true } chrono = { workspace = true } regex = { workspace = true } dirs = { workspace = true } -dark-light = { workspace = true } similar = { workspace = true } ignore = { workspace = true } urlencoding = { workspace = true } @@ -50,8 +49,6 @@ thiserror = "1.0" futures = { workspace = true } async-trait = { workspace = true } sha1 = { workspace = true } -screenshots = "0.8" -enigo = "0.2" image = { version = "0.24", default-features = false, features = ["png", "jpeg"] } resvg = { version = "0.47.0", default-features = false } @@ -79,6 +76,5 @@ windows = { version = "0.61.3", features = [ ] } windows-core = "0.61.2" -[target.'cfg(target_os = "linux")'.dependencies] -atspi = "0.29" +[target.'cfg(all(target_os = "linux"), not(target_env = "ohos))'.dependencies] leptess = "0.14.0" diff --git a/src/apps/desktop/capabilities/browser-webview.json b/src/apps/desktop/capabilities/browser-webview.json deleted file mode 100644 index 5fcbaf826..000000000 --- a/src/apps/desktop/capabilities/browser-webview.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "browser-webview", - "description": "Minimal permissions for embedded browser webviews to emit events back to the host", - "webviews": ["embedded-browser-*"], - "local": true, - "remote": { - "urls": ["https://*", "https://*:*", "http://*", "http://*:*"] - }, - "permissions": [ - "core:event:allow-emit" - ] -} diff --git a/src/apps/desktop/capabilities/default.json b/src/apps/desktop/capabilities/default.json index 945580903..0f22e9753 100644 --- a/src/apps/desktop/capabilities/default.json +++ b/src/apps/desktop/capabilities/default.json @@ -1,109 +1,108 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "BitFun default capabilities", - "windows": ["main"], - "permissions": [ - "log:default", - "autostart:default", - "core:default", - "core:path:default", - "core:event:default", - "core:event:allow-listen", - "core:event:allow-emit", - "core:window:default", - "core:webview:default", - "core:webview:allow-create-webview", - "core:webview:allow-set-webview-position", - "core:webview:allow-set-webview-size", - "core:webview:allow-set-webview-focus", - "core:webview:allow-reparent", - "core:webview:allow-webview-show", - "core:webview:allow-webview-hide", - "core:webview:allow-webview-close", - "core:window:allow-create", - "core:window:allow-set-focus", - "core:window:allow-set-always-on-top", - "core:window:allow-set-position", - "core:window:allow-set-size", - "core:window:allow-set-decorations", - "core:window:allow-set-title-bar-style", - "core:window:allow-set-skip-taskbar", - "core:window:allow-set-resizable", - "core:window:allow-current-monitor", - "core:window:allow-outer-position", - "core:window:allow-outer-size", - "core:window:allow-is-maximized", - "core:window:allow-center", - "core:window:allow-close", - "core:window:allow-hide", - "core:window:allow-maximize", - "core:window:allow-minimize", - "core:window:allow-show", - "core:window:allow-start-dragging", - "core:window:allow-unmaximize", - "core:window:allow-unminimize", - "core:window:allow-set-min-size", - "dialog:default", - "dialog:allow-open", - "dialog:allow-save", - "dialog:allow-ask", - "dialog:allow-confirm", - "dialog:allow-message", - "opener:default", - "opener:allow-open-url", - { - "identifier": "opener:allow-open-path", - "allow": [ - { "path": "$APPDATA/**" }, - { "path": "$HOME/**" } - ] - }, - "opener:allow-reveal-item-in-dir", - "fs:default", - "fs:allow-read-file", - "fs:allow-write-file", - "fs:allow-read-dir", - "fs:allow-copy-file", - "fs:allow-create", - "fs:allow-mkdir", - "fs:allow-remove", - "fs:allow-rename", - "fs:allow-exists", - "fs:allow-read-text-file", - "fs:allow-write-text-file", - "fs:allow-stat", - "fs:allow-lstat", - "fs:allow-fstat", - "fs:allow-truncate", - "fs:allow-ftruncate", - "fs:allow-open", - "fs:allow-read", - "fs:allow-write", - "fs:allow-seek", - "fs:allow-unwatch", - "fs:allow-watch", - "fs:read-all", - "fs:write-all", - "fs:scope-app-recursive", - { - "identifier": "fs:allow-home-read-recursive", - "allow": [ - { "path": "$HOME/**" } - ] - }, - { - "identifier": "fs:allow-home-write-recursive", - "allow": [ - { "path": "$HOME/**" } - ] - }, - "notification:default", - "notification:allow-notify", - "notification:allow-show", - "notification:allow-request-permission", - "notification:allow-check-permissions", - "notification:allow-permission-state", - "notification:allow-is-permission-granted" - ] -} + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "BitFun default capabilities", + "windows": ["main"], + "permissions": [ + "log:default", + "core:default", + "core:path:default", + "core:event:default", + "core:event:allow-listen", + "core:event:allow-emit", + "core:window:default", + "core:webview:default", + "core:webview:allow-create-webview", + "core:webview:allow-set-webview-position", + "core:webview:allow-set-webview-size", + "core:webview:allow-set-webview-focus", + "core:webview:allow-reparent", + "core:webview:allow-webview-show", + "core:webview:allow-webview-hide", + "core:webview:allow-webview-close", + "core:window:allow-create", + "core:window:allow-set-focus", + "core:window:allow-set-always-on-top", + "core:window:allow-set-position", + "core:window:allow-set-size", + "core:window:allow-set-decorations", + "core:window:allow-set-title-bar-style", + "core:window:allow-set-skip-taskbar", + "core:window:allow-set-resizable", + "core:window:allow-current-monitor", + "core:window:allow-outer-position", + "core:window:allow-outer-size", + "core:window:allow-is-maximized", + "core:window:allow-center", + "core:window:allow-close", + "core:window:allow-hide", + "core:window:allow-maximize", + "core:window:allow-minimize", + "core:window:allow-show", + "core:window:allow-start-dragging", + "core:window:allow-unmaximize", + "core:window:allow-unminimize", + "core:window:allow-set-min-size", + "dialog:default", + "dialog:allow-open", + "dialog:allow-save", + "dialog:allow-ask", + "dialog:allow-confirm", + "dialog:allow-message", + "opener:default", + "opener:allow-open-url", + { + "identifier": "opener:allow-open-path", + "allow": [ + { "path": "$APPDATA/**" }, + { "path": "$HOME/**" } + ] + }, + "opener:allow-reveal-item-in-dir", + "fs:default", + "fs:allow-read-file", + "fs:allow-write-file", + "fs:allow-read-dir", + "fs:allow-copy-file", + "fs:allow-create", + "fs:allow-mkdir", + "fs:allow-remove", + "fs:allow-rename", + "fs:allow-exists", + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "fs:allow-stat", + "fs:allow-lstat", + "fs:allow-fstat", + "fs:allow-truncate", + "fs:allow-ftruncate", + "fs:allow-open", + "fs:allow-read", + "fs:allow-write", + "fs:allow-seek", + "fs:allow-unwatch", + "fs:allow-watch", + "fs:read-all", + "fs:write-all", + "fs:scope-app-recursive", + { + "identifier": "fs:allow-home-read-recursive", + "allow": [ + { "path": "$HOME/**" } + ] + }, + { + "identifier": "fs:allow-home-write-recursive", + "allow": [ + { "path": "$HOME/**" } + ] + }, + "notification:default", + "notification:allow-notify", + "notification:allow-show", + "notification:allow-request-permission", + "notification:allow-check-permissions", + "notification:allow-permission-state", + "notification:allow-is-permission-granted" + ] +} \ No newline at end of file diff --git a/src/apps/desktop/src/api/browser_api.rs b/src/apps/desktop/src/api/browser_api.rs index b0189e156..b204f7f78 100644 --- a/src/apps/desktop/src/api/browser_api.rs +++ b/src/apps/desktop/src/api/browser_api.rs @@ -20,13 +20,7 @@ pub async fn browser_webview_eval( app: tauri::AppHandle, request: WebviewEvalRequest, ) -> Result<(), String> { - let webview = app - .get_webview(&request.label) - .ok_or_else(|| format!("Webview not found: {}", request.label))?; - - webview - .eval(&request.script) - .map_err(|e| format!("eval failed: {e}")) + Err("Webview not found".to_string()) } #[derive(Debug, Deserialize)] @@ -45,15 +39,5 @@ pub async fn browser_get_url( app: tauri::AppHandle, request: WebviewLabelRequest, ) -> Result { - let webview = app - .get_webview(&request.label) - .ok_or_else(|| format!("Webview not found: {}", request.label))?; - - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| webview.url())); - - match result { - Ok(Ok(url)) => Ok(url.to_string()), - Ok(Err(e)) => Err(format!("url failed: {e}")), - Err(_) => Err("url unavailable (webview URL is nil)".to_string()), - } + Err("Url is unavailable (webview URL is nil)".to_string()) } diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 4634d11c4..83cb1aeab 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -33,7 +33,7 @@ pub fn set_mobile_web_resource_path(path: PathBuf) { /// and restore any previously paired bot connections. Without this, bots /// only start listening after the user first opens the Remote Connect dialog. pub fn init_on_startup() { - tokio::spawn(async { + tauri::async_runtime::spawn(async { if let Err(e) = ensure_service().await { log::warn!("Remote connect startup init failed: {e}"); } diff --git a/src/apps/desktop/src/api/system_api.rs b/src/apps/desktop/src/api/system_api.rs index 975840019..6281b606f 100644 --- a/src/apps/desktop/src/api/system_api.rs +++ b/src/apps/desktop/src/api/system_api.rs @@ -180,11 +180,5 @@ pub async fn send_system_notification( app: tauri::AppHandle, request: SendNotificationRequest, ) -> Result<(), String> { - use tauri_plugin_notification::NotificationExt; - - let mut builder = app.notification().builder().title(&request.title); - if let Some(body) = &request.body { - builder = builder.body(body); - } - builder.show().map_err(|e| e.to_string()) + Err("No notification provided".to_string()) } diff --git a/src/apps/desktop/src/api/terminal_api.rs b/src/apps/desktop/src/api/terminal_api.rs index abd0b8e63..4995952d9 100644 --- a/src/apps/desktop/src/api/terminal_api.rs +++ b/src/apps/desktop/src/api/terminal_api.rs @@ -828,7 +828,7 @@ pub async fn terminal_get_history( } pub fn start_terminal_event_loop(terminal_state: TerminalState, app_handle: AppHandle) { - tokio::spawn(async move { + tauri::async_runtime::spawn(async move { let api = match terminal_state.get_or_init_api().await { Ok(api) => api, Err(e) => { diff --git a/src/apps/desktop/src/computer_use/desktop_host.rs b/src/apps/desktop/src/computer_use/desktop_host.rs index 1a84db911..7477f6d1a 100644 --- a/src/apps/desktop/src/computer_use/desktop_host.rs +++ b/src/apps/desktop/src/computer_use/desktop_host.rs @@ -1,5 +1,6 @@ //! Cross-platform `ComputerUseHost` via `screenshots` + `enigo`. +use anyhow::anyhow; use async_trait::async_trait; use bitfun_core::agentic::tools::computer_use_host::{ clamp_point_crop_half_extent, ActionRecord, AppClickParams, AppInfo, AppSelector, @@ -21,7 +22,6 @@ use bitfun_core::agentic::tools::computer_use_host::{ }; use bitfun_core::agentic::tools::computer_use_optimizer::ComputerUseOptimizer; use bitfun_core::util::errors::{BitFunError, BitFunResult}; -use enigo::{Axis, Button, Coordinate, Direction, Enigo, Key, Keyboard, Mouse, Settings}; use image::codecs::jpeg::JpegEncoder; use image::{DynamicImage, Rgb, RgbImage}; use log::{debug, info, warn}; @@ -49,7 +49,6 @@ const VISION_PIXEL_NUDGE_AFTER_SCREENSHOT_MSG: &str = "Computer use refused: do #[derive(Debug, Clone)] struct ScreenshotCacheEntry { rgba: image::RgbaImage, - screen: Screen, capture_time: Instant, } @@ -255,43 +254,6 @@ fn implicit_confirmation_should_apply( true } -fn global_to_native_full_pixel_center( - gx: f64, - gy: f64, - native_w: u32, - native_h: u32, - d: &DisplayInfo, -) -> (u32, u32) { - #[cfg(target_os = "macos")] - { - let geo = MacPointerGeo::from_display(native_w, native_h, d); - let lx = gx - geo.disp_ox; - let ly = gy - geo.disp_oy; - if lx < 0.0 || lx >= geo.disp_w || ly < 0.0 || ly >= geo.disp_h { - return clamp_center_to_native(native_w / 2, native_h / 2, native_w, native_h); - } - let full_ix = ((lx / geo.disp_w) * geo.full_px_w as f64).floor() as u32; - let full_iy = ((ly / geo.disp_h) * geo.full_px_h as f64).floor() as u32; - clamp_center_to_native(full_ix, full_iy, native_w, native_h) - } - #[cfg(not(target_os = "macos"))] - { - let disp_w = d.width as f64; - let disp_h = d.height as f64; - if disp_w <= 0.0 || disp_h <= 0.0 || native_w == 0 || native_h == 0 { - return (0, 0); - } - let lx = gx - d.x as f64; - let ly = gy - d.y as f64; - if lx < 0.0 || lx >= disp_w || ly < 0.0 || ly >= disp_h { - return clamp_center_to_native(native_w / 2, native_h / 2, native_w, native_h); - } - let full_ix = ((lx / disp_w) * native_w as f64).floor() as u32; - let full_iy = ((ly / disp_h) * native_h as f64).floor() as u32; - clamp_center_to_native(full_ix, full_iy, native_w, native_h) - } -} - #[cfg(target_os = "macos")] #[allow(dead_code)] fn implicit_global_center_for_confirmation( @@ -1036,41 +998,6 @@ end tell"#]) } } - fn with_enigo(f: F) -> BitFunResult - where - F: FnOnce(&mut Enigo) -> BitFunResult, - { - Self::ensure_input_automation_allowed()?; - let settings = Settings::default(); - let mut enigo = - Enigo::new(&settings).map_err(|e| BitFunError::tool(format!("enigo init: {}", e)))?; - f(&mut enigo) - } - - /// Enigo on macOS uses Text Input Source / AppKit paths that must run on the main queue. - /// Tokio `spawn_blocking` threads are not main; dispatch there hits `dispatch_assert_queue_fail`. - /// - /// On macOS, the main-queue dispatch is also wrapped in an Objective-C - /// `@try/@catch` (via `objc2::exception::catch`) so that an `NSException` - /// thrown by TSM / HIToolbox / AppKit during keyboard or text input is - /// converted into a Rust error instead of propagating across the FFI - /// boundary as a "foreign exception" — which would otherwise cause Rust's - /// `catch_unwind` to abort the whole process (`SIGABRT`). - fn run_enigo_job(job: F) -> BitFunResult - where - F: FnOnce(&mut Enigo) -> BitFunResult + Send, - T: Send, - { - #[cfg(target_os = "macos")] - { - macos::run_on_main_for_enigo(move || Self::with_enigo(job)) - } - #[cfg(not(target_os = "macos"))] - { - Self::with_enigo(job) - } - } - /// Absolute pointer move in Quartz global **points** with full float precision (avoids enigo integer truncation). #[cfg(target_os = "macos")] fn post_mouse_moved_cg_global(x: f64, y: f64) -> BitFunResult<()> { @@ -1135,90 +1062,15 @@ end tell"#]) const MAX_STEPS: usize = 85; const MAX_DURATION_MS: u64 = 400; - Self::run_enigo_job(|e| { - let (cx, cy) = e.location().map_err(|err| { - BitFunError::tool(format!("smooth_mouse_move: pointer location: {}", err)) - })?; - let x0 = cx as f64; - let y0 = cy as f64; - let dx = x1 - x0; - let dy = y1 - y0; - let dist = (dx * dx + dy * dy).sqrt(); - if dist < MIN_DIST { - return e - .move_mouse(x1.round() as i32, y1.round() as i32, Coordinate::Abs) - .map_err(|err| BitFunError::tool(format!("mouse_move: {}", err))); - } - let duration_ms = (70.0 + dist * 0.28).min(MAX_DURATION_MS as f64) as u64; - let steps = ((dist / 5.5).ceil() as usize).clamp(MIN_STEPS, MAX_STEPS); - let step_delay = Duration::from_millis((duration_ms / steps as u64).max(1)); - - for i in 1..=steps { - let t = i as f64 / steps as f64; - let te = Self::smoothstep01(t); - let x = x0 + dx * te; - let y = y0 + dy * te; - e.move_mouse(x.round() as i32, y.round() as i32, Coordinate::Abs) - .map_err(|err| BitFunError::tool(format!("mouse_move: {}", err)))?; - if i < steps { - std::thread::sleep(step_delay); - } - } - Ok(()) - }) + Ok(()) } - fn map_button(s: &str) -> BitFunResult + ); })} diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx index 316954193..73852c63c 100644 --- a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx +++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx @@ -12,7 +12,7 @@ import { Check, X } from 'lucide-react'; -import { open } from '@tauri-apps/plugin-dialog'; +// import { open } from '@tauri-apps/plugin-dialog'; import { useTranslation } from 'react-i18next'; import { createLogger } from '@/shared/utils/logger'; import { Modal, Button, Input } from '@/component-library'; @@ -49,13 +49,7 @@ export const NewProjectDialog: React.FC = ({ // Open directory picker dialog const handleSelectParentPath = useCallback(async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('newProject.selectParentDirectory'), - defaultPath: parentPath || defaultParentPath - }) as string; - + let selected = "/data/storage/el2/base/files"; if (selected && typeof selected === 'string') { setParentPath(selected); setError(''); diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts b/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts index 49cd6fdc4..9c0a0b2f7 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts +++ b/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts @@ -1,8 +1,10 @@ +import { storage } from "@/shared"; + export const REMOTE_CONNECT_DISCLAIMER_KEY = 'bitfun:remote-connect:disclaimer-agreed:v1'; export const getRemoteConnectDisclaimerAgreed = (): boolean => { try { - return localStorage.getItem(REMOTE_CONNECT_DISCLAIMER_KEY) === 'true'; + return storage.getItem(REMOTE_CONNECT_DISCLAIMER_KEY) === 'true'; } catch { return false; } @@ -10,7 +12,7 @@ export const getRemoteConnectDisclaimerAgreed = (): boolean => { export const setRemoteConnectDisclaimerAgreed = (): void => { try { - localStorage.setItem(REMOTE_CONNECT_DISCLAIMER_KEY, 'true'); + storage.setItem(REMOTE_CONNECT_DISCLAIMER_KEY, 'true'); } catch { // Ignore storage failures and fall back to in-memory state. } diff --git a/src/web-ui/src/app/layout/AppLayout.tsx b/src/web-ui/src/app/layout/AppLayout.tsx index e1a108119..2242a2e38 100644 --- a/src/web-ui/src/app/layout/AppLayout.tsx +++ b/src/web-ui/src/app/layout/AppLayout.tsx @@ -17,6 +17,7 @@ import { useApp } from '../hooks/useApp'; import { useSceneStore } from '../stores/sceneStore'; import { useShortcut } from '@/infrastructure/hooks/useShortcut'; import { configManager } from '@/infrastructure/config'; +import { sessionStorageAdapter } from '@/shared'; type TransitionDirection = 'entering' | 'returning' | null; import { FlowChatManager } from '../../flow_chat/services/FlowChatManager'; @@ -191,10 +192,10 @@ const AppLayout: React.FC = ({ className = '' }) => { // Always initialize FlowChat so historical sessions list even when SSH is not connected yet. try { const explicitPreferredMode = - sessionStorage.getItem('bitfun:flowchat:preferredMode') || + sessionStorageAdapter.getItem('bitfun:flowchat:preferredMode') || undefined; if (explicitPreferredMode) { - sessionStorage.removeItem('bitfun:flowchat:preferredMode'); + sessionStorageAdapter.removeItem('bitfun:flowchat:preferredMode'); } const initializationPreferredMode = @@ -229,9 +230,9 @@ const AppLayout: React.FC = ({ className = '' }) => { ensureAssistantBootstrapForWorkspace(currentWorkspace, activeSessionId); } - const pendingDescription = sessionStorage.getItem('pendingProjectDescription'); + const pendingDescription = sessionStorageAdapter.getItem('pendingProjectDescription'); if (pendingDescription && pendingDescription.trim()) { - sessionStorage.removeItem('pendingProjectDescription'); + sessionStorageAdapter.removeItem('pendingProjectDescription'); setTimeout(async () => { try { @@ -257,9 +258,9 @@ const AppLayout: React.FC = ({ className = '' }) => { }, 500); } - const pendingSettings = sessionStorage.getItem('pendingOpenSettings'); + const pendingSettings = sessionStorageAdapter.getItem('pendingOpenSettings'); if (pendingSettings) { - sessionStorage.removeItem('pendingOpenSettings'); + sessionStorageAdapter.removeItem('pendingOpenSettings'); setTimeout(async () => { try { const { quickActions } = await import('@/shared/services/ide-control'); diff --git a/src/web-ui/src/app/layout/panelConfig.ts b/src/web-ui/src/app/layout/panelConfig.ts index 3c2f07487..40c730f40 100644 --- a/src/web-ui/src/app/layout/panelConfig.ts +++ b/src/web-ui/src/app/layout/panelConfig.ts @@ -13,6 +13,7 @@ * - expanded: expanded mode (wide content, more information) */ +import { storage } from '@/shared'; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('PanelConfig'); @@ -194,7 +195,7 @@ export const STORAGE_KEYS = { */ export function savePanelWidth(key: string, width: number): void { try { - localStorage.setItem(key, String(width)); + storage.setItem(key, String(width)); } catch (e) { log.warn('Failed to save panel width', { key, width, error: e }); } @@ -205,7 +206,7 @@ export function savePanelWidth(key: string, width: number): void { */ export function loadPanelWidth(key: string, defaultValue: number): number { try { - const stored = localStorage.getItem(key); + const stored = storage.getItem(key); if (stored) { const parsed = parseInt(stored, 10); if (!isNaN(parsed) && parsed > 0) { diff --git a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx index ca5ac675f..2f42d1ac8 100644 --- a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx +++ b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx @@ -55,12 +55,7 @@ const WelcomeScene: React.FC = () => { const handleOpenFolder = useCallback(async () => { try { setIsSelecting(true); - const { open } = await import('@tauri-apps/plugin-dialog'); - const selected = await open({ - directory: true, - multiple: false, - title: t('startup.selectWorkspaceDirectory'), - }); + let selected = "/data/storage/el2/base/files/test"; if (selected && typeof selected === 'string') { await openWorkspace(selected); openScene('session' as SceneTabId); diff --git a/src/web-ui/src/app/services/AppManager.ts b/src/web-ui/src/app/services/AppManager.ts index 430e511ee..e72726408 100644 --- a/src/web-ui/src/app/services/AppManager.ts +++ b/src/web-ui/src/app/services/AppManager.ts @@ -18,6 +18,7 @@ import { import { globalEventBus } from '../../infrastructure/event-bus'; import { createLogger } from '@/shared/utils/logger'; import { i18nService } from '@/infrastructure/i18n'; +import { storage } from '@/shared'; const log = createLogger('AppManager'); @@ -351,13 +352,13 @@ export class AppManager implements IAppManager { private clearPersistedPanelState(): void { try { // Clear AppManager persisted state - localStorage.removeItem('bitfun-app-state'); + storage.removeItem('bitfun-app-state'); // Clear other potential panel state keys - localStorage.removeItem('BitFun-left-panel-width'); - localStorage.removeItem('BitFun-left-panel-collapsed'); - localStorage.removeItem('BitFun-right-panel-collapsed'); - localStorage.removeItem('right-panel-collapsed'); + storage.removeItem('BitFun-left-panel-width'); + storage.removeItem('BitFun-left-panel-collapsed'); + storage.removeItem('BitFun-right-panel-collapsed'); + storage.removeItem('right-panel-collapsed'); } catch (error) { log.warn('Failed to clear persisted panel state', error); } diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 818770cc8..74780f51c 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -58,6 +58,7 @@ import { useDeepReviewConsent } from './DeepReviewConsentDialog'; import { useSessionReviewActivity } from '../hooks/useSessionReviewActivity'; import { shouldBlockDeepReviewCommand } from '../utils/deepReviewCommandGuard'; import './ChatInput.scss'; +import { sessionStorageAdapter } from '@/shared'; const log = createLogger('ChatInput'); @@ -782,7 +783,7 @@ export const ChatInput: React.FC = ({ log.debug('Session switched, syncing mode', { sessionId, mode }); dispatchMode({ type: 'SET_CURRENT_MODE', payload: mode }); try { - sessionStorage.setItem('bitfun:flowchat:lastMode', mode); + sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', mode); } catch { // ignore } @@ -812,7 +813,7 @@ export const ChatInput: React.FC = ({ }); dispatchMode({ type: 'SET_CURRENT_MODE', payload: nextMode }); try { - sessionStorage.setItem('bitfun:flowchat:lastMode', nextMode); + sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', session.mode); } catch { // ignore } @@ -1700,7 +1701,7 @@ export const ChatInput: React.FC = ({ }); try { - sessionStorage.setItem('bitfun:flowchat:lastMode', modeId); + sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', modeId); } catch { // ignore } diff --git a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx index 2283a6ab8..a3071db85 100644 --- a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx +++ b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx @@ -151,9 +151,8 @@ export const WelcomePanel: React.FC = ({ try { setWorkspaceDropdownOpen(false); setIsSelectingWorkspace(true); - const { open } = await import('@tauri-apps/plugin-dialog'); - const selected = await open({ directory: true, multiple: false }); - if (selected && typeof selected === 'string') await openWorkspace(selected); + let path_manager = "/data/storage/el2/base/files/test"; + await openWorkspace(path_manager); } catch (err) { log.warn('Failed to open workspace folder', err); } finally { diff --git a/src/web-ui/src/flow_chat/store/FlowChatStore.ts b/src/web-ui/src/flow_chat/store/FlowChatStore.ts index 7545a0775..48cff3bba 100644 --- a/src/web-ui/src/flow_chat/store/FlowChatStore.ts +++ b/src/web-ui/src/flow_chat/store/FlowChatStore.ts @@ -40,6 +40,7 @@ import { import type { WorkspaceInfo } from '@/shared/types'; import { sessionBelongsToWorkspaceNavRow } from '../utils/sessionOrdering'; import { sessionMatchesWorkspace } from '../utils/workspaceScope'; +import { storage } from '@/shared'; const log = createLogger('FlowChatStore'); @@ -47,7 +48,7 @@ export class FlowChatStore { private static instance: FlowChatStore; private state: FlowChatState; private listeners: Set<(state: FlowChatState) => void> = new Set(); - + private silentMode = false; private constructor() { @@ -65,18 +66,23 @@ export class FlowChatStore { 'bitfun-flow-chat-global', 'bitfun-session-ids' ]; - + keysToRemove.forEach(key => { - if (localStorage.getItem(key)) { - localStorage.removeItem(key); + if (storage.getItem(key)) { + storage.removeItem(key); } }); + try { + const keys = storage.getKeys(); + keys.forEach(key => { + if (key.startsWith('bitfun-session-')) { + storage.removeItem(key); + } + }) + } catch (e) { + log.warn("Failed to clear session key"); + } - Object.keys(localStorage).forEach(key => { - if (key.startsWith('bitfun-session-')) { - localStorage.removeItem(key); - } - }); } catch (error) { log.warn('Failed to clear old storage data', error); } @@ -97,7 +103,7 @@ export class FlowChatStore { public setState(updater: (prevState: FlowChatState) => FlowChatState): void { const newState = updater(this.state); this.state = newState; - + if (!this.silentMode) { this.listeners.forEach(listener => { try { @@ -108,7 +114,7 @@ export class FlowChatStore { }); } } - + /** * Silent state update (does not trigger listeners) * Used for batch updates, call notifyListeners() after completion @@ -122,7 +128,7 @@ export class FlowChatStore { this.silentMode = prevSilentMode; } } - + /** * Manually notify all listeners (call after batch updates complete) */ @@ -135,11 +141,11 @@ export class FlowChatStore { } }); } - + public beginSilentMode(): void { this.silentMode = true; } - + public endSilentMode(): void { this.silentMode = false; this.notifyListeners(); @@ -211,7 +217,7 @@ export class FlowChatStore { import('../state-machine').then(({ stateMachineManager }) => { stateMachineManager.getOrCreate(sessionId); }); - + this.setState(prev => { const relationship = normalizeSessionRelationship({ sessionKind: 'normal' }); const titleState = deriveSessionTitleState(titleDescriptor); @@ -316,18 +322,18 @@ export class FlowChatStore { public switchSession(sessionId: string): void { let sessionMode: string | undefined; - + this.setState(prev => { if (!prev.sessions.has(sessionId)) return prev; - + const session = prev.sessions.get(sessionId)!; sessionMode = session.mode; - + const updatedSession = { ...session, lastActiveAt: Date.now() }; - + const newSessions = new Map(prev.sessions); newSessions.set(sessionId, updatedSession); @@ -337,7 +343,7 @@ export class FlowChatStore { activeSessionId: sessionId }; }); - + window.dispatchEvent(new CustomEvent('bitfun:session-switched', { detail: { sessionId, mode: sessionMode || 'agentic' } })); @@ -832,7 +838,7 @@ export class FlowChatStore { const session = prev.sessions.get(sessionId); if (!session) return prev; - const updatedDialogTurns = session.dialogTurns.map(turn => + const updatedDialogTurns = session.dialogTurns.map(turn => turn.id === dialogTurnId ? updater(turn) : turn ); @@ -856,8 +862,8 @@ export class FlowChatStore { * Add image analysis phase to dialog turn */ public addImageAnalysisPhase( - sessionId: string, - dialogTurnId: string, + sessionId: string, + dialogTurnId: string, imageContexts: import('@/shared/types/context').ImageContext[] ): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => { @@ -890,11 +896,11 @@ export class FlowChatStore { dialogTurnId: string, results: ImageAnalysisResult[] ): void { - this.updateDialogTurn(sessionId, dialogTurnId, turn => { - if (!turn.imageAnalysisPhase) { - log.warn('Attempting to update non-existent image analysis phase', { sessionId, dialogTurnId }); - return turn; - } + this.updateDialogTurn(sessionId, dialogTurnId, turn => { + if (!turn.imageAnalysisPhase) { + log.warn('Attempting to update non-existent image analysis phase', { sessionId, dialogTurnId }); + return turn; + } const updatedItems: FlowImageAnalysisItem[] = turn.imageAnalysisPhase.items.map(item => { const result = results.find(r => r.image_id === item.imageContext.id); @@ -963,7 +969,7 @@ export class FlowChatStore { public updateModelRound(sessionId: string, dialogTurnId: string, modelRoundId: string, updater: (round: ModelRound) => ModelRound): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => ({ ...turn, - modelRounds: turn.modelRounds.map(round => + modelRounds: turn.modelRounds.map(round => round.id === modelRoundId ? updater(round) : round ) })); @@ -973,12 +979,12 @@ export class FlowChatStore { * Batch update multiple model round items (reduces store update frequency) */ public batchUpdateModelRoundItems( - sessionId: string, - dialogTurnId: string, + sessionId: string, + dialogTurnId: string, updates: Array<{ itemId: string; changes: Partial }> ): void { if (updates.length === 0) return; - + this.updateDialogTurn(sessionId, dialogTurnId, turn => { const updatedModelRounds = turn.modelRounds.map(round => ({ ...round, @@ -987,7 +993,7 @@ export class FlowChatStore { return update ? ({ ...item, ...update.changes } as AnyFlowItem) : item; }) })); - + return { ...turn, modelRounds: updatedModelRounds @@ -998,18 +1004,18 @@ export class FlowChatStore { public addModelRoundItem(sessionId: string, dialogTurnId: string, item: AnyFlowItem, modelRoundId?: string): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => { let targetModelRoundIndex = turn.modelRounds.length - 1; - if (modelRoundId) { - targetModelRoundIndex = turn.modelRounds.findIndex(round => round.id === modelRoundId); - if (targetModelRoundIndex === -1) { - log.warn('Model round not found', { sessionId, dialogTurnId, modelRoundId }); - return turn; - } - } - + if (modelRoundId) { + targetModelRoundIndex = turn.modelRounds.findIndex(round => round.id === modelRoundId); if (targetModelRoundIndex === -1) { - log.warn('No available model rounds', { sessionId, dialogTurnId }); + log.warn('Model round not found', { sessionId, dialogTurnId, modelRoundId }); return turn; } + } + + if (targetModelRoundIndex === -1) { + log.warn('No available model rounds', { sessionId, dialogTurnId }); + return turn; + } const targetModelRound = turn.modelRounds[targetModelRoundIndex]; @@ -1019,7 +1025,7 @@ export class FlowChatStore { } const updatedModelRounds = [...turn.modelRounds]; - + updatedModelRounds[targetModelRoundIndex] = { ...targetModelRound, items: [...targetModelRound.items, item] @@ -1057,7 +1063,7 @@ export class FlowChatStore { this.updateDialogTurn(sessionId, dialogTurnId, turn => { let parentRoundIndex = -1; let parentItemIndex = -1; - + for (let i = 0; i < turn.modelRounds.length; i++) { const itemIndex = turn.modelRounds[i].items.findIndex((item: any) => item.id === parentToolId); if (itemIndex !== -1) { @@ -1066,21 +1072,21 @@ export class FlowChatStore { break; } } - + if (parentRoundIndex === -1 || parentItemIndex === -1) { log.warn('Parent tool item not found', { sessionId, dialogTurnId, parentToolId }); return turn; } - + const targetModelRound = turn.modelRounds[parentRoundIndex]; - + const existingItem = targetModelRound.items.find((item: any) => item.id === newItem.id); if (existingItem) { return turn; } - + let insertIndex = parentItemIndex + 1; - + while (insertIndex < targetModelRound.items.length) { const currentItem = targetModelRound.items[insertIndex] as any; if (currentItem.parentTaskToolId === parentToolId && currentItem.isSubagentItem) { @@ -1089,19 +1095,19 @@ export class FlowChatStore { break; } } - + const updatedItems = [ ...targetModelRound.items.slice(0, insertIndex), newItem, ...targetModelRound.items.slice(insertIndex) ]; - + const updatedModelRounds = [...turn.modelRounds]; updatedModelRounds[parentRoundIndex] = { ...targetModelRound, items: updatedItems }; - + return { ...turn, modelRounds: updatedModelRounds @@ -1112,10 +1118,10 @@ export class FlowChatStore { public updateModelRoundItem(sessionId: string, dialogTurnId: string, itemId: string, updates: Partial): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => { let updated = false; - + const updatedModelRounds = turn.modelRounds.map(modelRound => { if (updated) return modelRound; - + const updatedItems = modelRound.items.map((item: any) => { if (item.id === itemId) { const updatedItem = { ...item, ...updates }; @@ -1123,15 +1129,15 @@ export class FlowChatStore { } return item; }); - + if (updatedItems.some((item: any) => item.id === itemId)) { updated = true; return { ...modelRound, items: updatedItems }; } - + return modelRound; }); - + if (!updated) { log.warn('Item not found for update', { sessionId, dialogTurnId, itemId }); return turn; @@ -1179,7 +1185,7 @@ export class FlowChatStore { } public updateTokenUsage( - sessionId: string, + sessionId: string, tokenUsage: { inputTokens: number; outputTokens?: number; totalTokens: number } ): void { this.setState(prev => { @@ -1421,7 +1427,7 @@ export class FlowChatStore { } const turnIndex = session.dialogTurns.findIndex(t => t.id === turnId); - + const turnData = { turnId, turnIndex, @@ -1444,7 +1450,7 @@ export class FlowChatStore { timestamp: item.timestamp, status: item.status, })); - + const toolItems = round.items .filter(item => item.type === 'tool') .map(item => ({ @@ -1457,11 +1463,11 @@ export class FlowChatStore { startTime: (item as any).startTime || item.timestamp, endTime: (item as any).endTime, status: item.status, - durationMs: (item as any).endTime - ? (item as any).endTime - (item as any).startTime + durationMs: (item as any).endTime + ? (item as any).endTime - (item as any).startTime : undefined })); - + const thinkingItems = round.items .filter(item => item.type === 'thinking') .map(item => ({ @@ -1472,7 +1478,7 @@ export class FlowChatStore { timestamp: item.timestamp, status: item.status, })); - + return { id: round.id, turnId, @@ -1521,29 +1527,29 @@ export class FlowChatStore { sessions.forEach(metadata => { stateMachineManager.getOrCreate(metadata.sessionId); }); - + const processSession = async (metadata: any) => { const existingSession = this.state.sessions.get(metadata.sessionId); if (existingSession) { return; } - + let maxContextTokens = 128128; try { const { configManager } = await import('@/infrastructure/config/services/ConfigManager'); const models = await configManager.getConfig('ai.models') || []; - + if (metadata.modelName) { const model = models.find((m: any) => m.name === metadata.modelName || m.id === metadata.modelName); if (model?.context_window) { maxContextTokens = model.context_window; } } - + if (maxContextTokens === 128128) { const defaultModels = await configManager.getConfig>('ai.default_models'); const primaryModelId = defaultModels?.primary; - + if (primaryModelId) { const primaryModel = models.find((m: any) => m.id === primaryModelId); if (primaryModel?.context_window) { @@ -1554,7 +1560,7 @@ export class FlowChatStore { } catch (error) { log.warn('Failed to get model context window size, using default', { sessionId: metadata.sessionId, error }); } - + const relationship = deriveSessionRelationshipFromMetadata(metadata); const lastFinishedAt = deriveLastFinishedAtFromMetadata(metadata); const titleState = deriveSessionTitleStateFromMetadata(metadata); @@ -1564,15 +1570,15 @@ export class FlowChatStore { if (prev.sessions.has(metadata.sessionId)) { return prev; } - + const VALID_AGENT_TYPES = ['agentic', 'debug', 'Plan', 'Cowork', 'Claw', 'Team', 'DeepResearch']; const rawAgentType = metadata.agentType || 'agentic'; const validatedAgentType = VALID_AGENT_TYPES.includes(rawAgentType) ? rawAgentType : 'agentic'; - + if (rawAgentType !== validatedAgentType) { log.warn('Invalid agentType, falling back to agentic', { sessionId: metadata.sessionId, rawAgentType, validatedAgentType }); } - + const session: Session = { sessionId: metadata.sessionId, title: titleState.title, @@ -1603,17 +1609,17 @@ export class FlowChatStore { btwThreads: [], btwOrigin: relationship.btwOrigin, }; - + const newSessions = new Map(prev.sessions); newSessions.set(metadata.sessionId, session); - + return { ...prev, sessions: newSessions, }; }); }; - + await Promise.all(sessions.map(processSession)); } catch (error) { log.error('Failed to load persisted sessions', error); @@ -1633,14 +1639,14 @@ export class FlowChatStore { try { const { stateMachineManager } = await import('../state-machine'); stateMachineManager.getOrCreate(sessionId); - + try { const { agentAPI } = await import('@/infrastructure/api'); await agentAPI.restoreSession(sessionId, workspacePath, remoteConnectionId, remoteSshHost); } catch (error) { log.warn('Backend session restore failed (may be new session)', { sessionId, error }); } - + const { sessionAPI } = await import('@/infrastructure/api'); const turns = await sessionAPI.loadSessionTurns( sessionId, @@ -1649,22 +1655,22 @@ export class FlowChatStore { remoteConnectionId, remoteSshHost ); - + const dialogTurns = this.convertToDialogTurns(turns); - + this.setState(prev => { const session = prev.sessions.get(sessionId); if (!session) return prev; - + const updatedSession = { ...session, dialogTurns, isHistorical: false, }; - + const newSessions = new Map(prev.sessions); newSessions.set(sessionId, updatedSession); - + return { ...prev, sessions: newSessions, @@ -1701,12 +1707,12 @@ export class FlowChatStore { const hasImages = Array.isArray(metaImages) && metaImages.length > 0; const images = hasImages ? metaImages.map((img: any) => ({ - id: img.id || img.name || `img-${Date.now()}`, - name: img.name || 'image', - dataUrl: img.data_url, - imagePath: img.image_path, - mimeType: img.mime_type, - })) + id: img.id || img.name || `img-${Date.now()}`, + name: img.name || 'image', + dataUrl: img.data_url, + imagePath: img.image_path, + mimeType: img.mime_type, + })) : undefined; const displayContent = @@ -1714,99 +1720,99 @@ export class FlowChatStore { const normalizedTurnStatus = normalizeRecoveredTurnStatus(turn.status, { error: undefined }); return { - id: turn.turnId, - sessionId: turn.sessionId, - kind: turn.kind || 'user_dialog', - userMessage: { - id: turn.userMessage.id, - type: 'user' as const, - content: displayContent, - timestamp: turn.userMessage.timestamp, - hasImages, - metadata, - images, - }, - modelRounds: turn.modelRounds.map((round: any) => { - const normalizedRoundStatus = normalizeRecoveredRoundStatus(round.status, normalizedTurnStatus); + id: turn.turnId, + sessionId: turn.sessionId, + kind: turn.kind || 'user_dialog', + userMessage: { + id: turn.userMessage.id, + type: 'user' as const, + content: displayContent, + timestamp: turn.userMessage.timestamp, + hasImages, + metadata, + images, + }, + modelRounds: turn.modelRounds.map((round: any) => { + const normalizedRoundStatus = normalizeRecoveredRoundStatus(round.status, normalizedTurnStatus); - return { - id: round.id, - turnId: round.turnId, - index: round.roundIndex ?? 0, - items: [ - ...round.textItems.map((text: any) => ({ - id: text.id, - type: 'text' as const, - content: text.content, - isStreaming: false, - isMarkdown: text.isMarkdown !== undefined ? text.isMarkdown : true, - timestamp: text.timestamp, - status: normalizeRecoveredTextStatus(text.status, normalizedTurnStatus), - orderIndex: text.orderIndex, - isSubagentItem: text.isSubagentItem, - parentTaskToolId: text.parentTaskToolId, - subagentSessionId: text.subagentSessionId, - })), - ...round.toolItems.map((tool: any) => ({ - id: tool.id, - type: 'tool' as const, - toolName: tool.toolName, - interruptionReason: - tool.interruptionReason === 'app_restart' - ? 'app_restart' - : isTransientToolStatus(tool.status) + return { + id: round.id, + turnId: round.turnId, + index: round.roundIndex ?? 0, + items: [ + ...round.textItems.map((text: any) => ({ + id: text.id, + type: 'text' as const, + content: text.content, + isStreaming: false, + isMarkdown: text.isMarkdown !== undefined ? text.isMarkdown : true, + timestamp: text.timestamp, + status: normalizeRecoveredTextStatus(text.status, normalizedTurnStatus), + orderIndex: text.orderIndex, + isSubagentItem: text.isSubagentItem, + parentTaskToolId: text.parentTaskToolId, + subagentSessionId: text.subagentSessionId, + })), + ...round.toolItems.map((tool: any) => ({ + id: tool.id, + type: 'tool' as const, + toolName: tool.toolName, + interruptionReason: + tool.interruptionReason === 'app_restart' ? 'app_restart' - : undefined, - toolCall: tool.toolCall, - toolResult: tool.toolResult, - aiIntent: tool.aiIntent, - startTime: tool.startTime, - endTime: tool.endTime, - timestamp: tool.startTime, - status: normalizeRecoveredToolStatus( - tool.status, - normalizedTurnStatus, - tool.toolResult, - { preservePendingConfirmation: true }, - ), - orderIndex: tool.orderIndex, - isSubagentItem: tool.isSubagentItem, - parentTaskToolId: tool.parentTaskToolId, - subagentSessionId: tool.subagentSessionId, - })), - ...(round.thinkingItems || []).map((thinking: any) => ({ - id: thinking.id, - type: 'thinking' as const, - content: thinking.content, - isStreaming: false, - isCollapsed: thinking.isCollapsed ?? true, - timestamp: thinking.timestamp, - status: normalizeRecoveredThinkingStatus(thinking.status, normalizedTurnStatus), - orderIndex: thinking.orderIndex, - isSubagentItem: thinking.isSubagentItem, - parentTaskToolId: thinking.parentTaskToolId, - subagentSessionId: thinking.subagentSessionId, - })), - ].sort((a: any, b: any) => { - const aIndex = a.orderIndex !== undefined ? a.orderIndex : a.timestamp || 0; - const bIndex = b.orderIndex !== undefined ? b.orderIndex : b.timestamp || 0; - - return aIndex - bIndex; - }), - isStreaming: false, - isComplete: normalizedRoundStatus !== 'pending' && normalizedRoundStatus !== 'streaming', - status: normalizedRoundStatus, - startTime: round.startTime ?? round.timestamp, - endTime: round.endTime, - timestamp: round.timestamp, - }; - }), - timestamp: turn.timestamp, - status: normalizedTurnStatus, - startTime: turn.startTime, - endTime: turn.endTime, - backendTurnIndex: turn.turnIndex, - }; + : isTransientToolStatus(tool.status) + ? 'app_restart' + : undefined, + toolCall: tool.toolCall, + toolResult: tool.toolResult, + aiIntent: tool.aiIntent, + startTime: tool.startTime, + endTime: tool.endTime, + timestamp: tool.startTime, + status: normalizeRecoveredToolStatus( + tool.status, + normalizedTurnStatus, + tool.toolResult, + { preservePendingConfirmation: true }, + ), + orderIndex: tool.orderIndex, + isSubagentItem: tool.isSubagentItem, + parentTaskToolId: tool.parentTaskToolId, + subagentSessionId: tool.subagentSessionId, + })), + ...(round.thinkingItems || []).map((thinking: any) => ({ + id: thinking.id, + type: 'thinking' as const, + content: thinking.content, + isStreaming: false, + isCollapsed: thinking.isCollapsed ?? true, + timestamp: thinking.timestamp, + status: normalizeRecoveredThinkingStatus(thinking.status, normalizedTurnStatus), + orderIndex: thinking.orderIndex, + isSubagentItem: thinking.isSubagentItem, + parentTaskToolId: thinking.parentTaskToolId, + subagentSessionId: thinking.subagentSessionId, + })), + ].sort((a: any, b: any) => { + const aIndex = a.orderIndex !== undefined ? a.orderIndex : a.timestamp || 0; + const bIndex = b.orderIndex !== undefined ? b.orderIndex : b.timestamp || 0; + + return aIndex - bIndex; + }), + isStreaming: false, + isComplete: normalizedRoundStatus !== 'pending' && normalizedRoundStatus !== 'streaming', + status: normalizedRoundStatus, + startTime: round.startTime ?? round.timestamp, + endTime: round.endTime, + timestamp: round.timestamp, + }; + }), + timestamp: turn.timestamp, + status: normalizedTurnStatus, + startTime: turn.startTime, + endTime: turn.endTime, + backendTurnIndex: turn.turnIndex, + }; }); } @@ -1853,7 +1859,7 @@ export class FlowChatStore { const turn = session.dialogTurns.find(t => t.id === turnId); return turn?.todos || []; } - + public deleteTodo(sessionId: string, todoId: string): void { this.setState(prev => { const session = prev.sessions.get(sessionId); @@ -1888,18 +1894,18 @@ export class FlowChatStore { public getTodos(sessionId: string): import('../types/flow-chat').TodoItem[] { const session = this.state.sessions.get(sessionId); if (!session) return []; - + const allTodos: import('../types/flow-chat').TodoItem[] = []; session.dialogTurns.forEach(turn => { if (turn.todos && turn.todos.length > 0) { allTodos.push(...turn.todos); } }); - + if (session.todos && session.todos.length > 0) { allTodos.push(...session.todos); } - + return allTodos; } diff --git a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts index c581c5b50..28e0012f2 100644 --- a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts +++ b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts @@ -4,8 +4,9 @@ * History is now session-scoped - each session maintains its own input history. */ +import { storage } from '@/shared'; import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { createJSONStorage, persist } from 'zustand/middleware'; export interface InputHistoryState { /** Map of sessionId to list of previously sent messages (most recent first) */ @@ -92,6 +93,7 @@ export const useInputHistoryStore = create()( { name: 'bitfun-input-history', version: 2, // Bump version to migrate from old format + storage: createJSONStorage(() => storage), migrate: (persistedState: any, version: number) => { if (version < 2) { // Migrate from old global format to new session-scoped format diff --git a/src/web-ui/src/hooks/useModelConfigs.ts b/src/web-ui/src/hooks/useModelConfigs.ts index 960cb1bc4..35cc22a8f 100644 --- a/src/web-ui/src/hooks/useModelConfigs.ts +++ b/src/web-ui/src/hooks/useModelConfigs.ts @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { ModelConfig } from '../shared/types'; import { modelConfigManager } from '../infrastructure/config/services/modelConfigs'; +import { storage } from '@/shared'; export const useModelConfigs = () => { const [configs, setConfigs] = useState([]); const [loading, setLoading] = useState(true); @@ -37,9 +38,9 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { const setCurrentConfigWithPersistence = (config: ModelConfig | null) => { setCurrentConfig(config); if (config) { - localStorage.setItem(CURRENT_CONFIG_KEY, config.id); + storage.setItem(CURRENT_CONFIG_KEY, config.id); } else { - localStorage.removeItem(CURRENT_CONFIG_KEY); + storage.removeItem(CURRENT_CONFIG_KEY); } // Notify other components via storage event @@ -75,14 +76,14 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { } if (!currentConfig) { - const savedConfigId = localStorage.getItem(CURRENT_CONFIG_KEY); + const savedConfigId = storage.getItem(CURRENT_CONFIG_KEY); const targetConfigId = initialConfigId || savedConfigId; if (targetConfigId) { const foundConfig = configs.find(c => c.id === targetConfigId); if (foundConfig) { setCurrentConfig(foundConfig); - localStorage.setItem(CURRENT_CONFIG_KEY, foundConfig.id); + storage.setItem(CURRENT_CONFIG_KEY, foundConfig.id); return; } } @@ -91,7 +92,7 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { const firstConfig = configs[0]; if (firstConfig) { setCurrentConfig(firstConfig); - localStorage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); + storage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); } return; } @@ -102,10 +103,10 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { const firstConfig = configs[0]; if (firstConfig) { setCurrentConfig(firstConfig); - localStorage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); + storage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); } else { setCurrentConfig(null); - localStorage.removeItem(CURRENT_CONFIG_KEY); + storage.removeItem(CURRENT_CONFIG_KEY); } } else { // Sync with latest version (config may have been edited) diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx index b1eb987e6..5cf718132 100644 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx @@ -8,6 +8,7 @@ import { lspService } from '@/tools/lsp/services/LspService'; import { open } from '@tauri-apps/plugin-dialog'; import { createLogger } from '@/shared/utils/logger'; import './LspConfig.scss'; +import { storage } from '@/shared'; const log = createLogger('LspConfig'); @@ -29,7 +30,7 @@ const LspConfig: React.FC = () => { function loadSettings() { try { - const saved = localStorage.getItem(LSP_SETTINGS_KEY); + const saved = storage.getItem(LSP_SETTINGS_KEY); if (saved) setSettings({ ...DEFAULT_LSP_SETTINGS, ...JSON.parse(saved) }); } catch (error) { log.error('Failed to load settings', error); @@ -40,7 +41,7 @@ const LspConfig: React.FC = () => { const saveSettings = () => { try { - localStorage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); + storage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); setHasSettingsChanges(false); setInstallMessage({ type: 'success', text: t('messages.settingsSaved') }); setTimeout(() => setInstallMessage(null), 2000); diff --git a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts index df26b6a9f..133fab981 100644 --- a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts +++ b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts @@ -1,9 +1,10 @@ import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { persist, createJSONStorage } from 'zustand/middleware'; import type { LocaleId, I18nNamespace, I18nState, I18nActions } from '../types'; import { DEFAULT_LOCALE, DEFAULT_FALLBACK_LOCALE } from '../presets'; +import { storage } from '@/shared'; const initialState: I18nState = { @@ -59,6 +60,7 @@ export const useI18nStore = create()( }), { name: 'bitfun-i18n-state', + storage: createJSONStorage(() => storage), partialize: (state) => ({ currentLanguage: state.currentLanguage, fallbackLanguage: state.fallbackLanguage, diff --git a/src/web-ui/src/shared/stores/contextStore.ts b/src/web-ui/src/shared/stores/contextStore.ts index 05925dcb8..215c64afd 100644 --- a/src/web-ui/src/shared/stores/contextStore.ts +++ b/src/web-ui/src/shared/stores/contextStore.ts @@ -1,24 +1,25 @@ - + import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; +import { devtools, persist, StorageValue } from 'zustand/middleware'; import { ContextItem, ValidationResult } from '../types/context'; import { createLogger } from '@/shared/utils/logger'; +import { storage } from '@/shared/utils/storageAdapter'; const log = createLogger('ContextStore'); interface ContextState { - + contexts: ContextItem[]; - - + + validationStates: Map; - - + + validatingIds: Set; - + // Actions addContext: (item: ContextItem) => void; removeContext: (id: string) => void; @@ -35,35 +36,35 @@ export const useContextStore = create()( devtools( persist( (set, _get) => ({ - + contexts: [], validationStates: new Map(), validatingIds: new Set(), - - + + addContext: (item: ContextItem) => { set((state) => { - + if (state.contexts.some(c => c.id === item.id)) { log.warn('Context already exists', { id: item.id }); return state; } - + return { contexts: [...state.contexts, item] }; }, false, 'addContext'); }, - - + + removeContext: (id: string) => { set((state) => { const newValidationStates = new Map(state.validationStates); newValidationStates.delete(id); - + const newValidatingIds = new Set(state.validatingIds); newValidatingIds.delete(id); - + return { contexts: state.contexts.filter(c => c.id !== id), validationStates: newValidationStates, @@ -71,8 +72,8 @@ export const useContextStore = create()( }; }, false, 'removeContext'); }, - - + + clearContexts: () => { set({ contexts: [], @@ -80,24 +81,24 @@ export const useContextStore = create()( validatingIds: new Set() }, false, 'clearContexts'); }, - - + + updateValidation: (id: string, result: ValidationResult) => { set((state) => { const newValidationStates = new Map(state.validationStates); newValidationStates.set(id, result); - + const newValidatingIds = new Set(state.validatingIds); newValidatingIds.delete(id); - + return { validationStates: newValidationStates, validatingIds: newValidatingIds }; }, false, 'updateValidation'); }, - - + + setValidating: (id: string, validating: boolean) => { set((state) => { const newValidatingIds = new Set(state.validatingIds); @@ -106,36 +107,58 @@ export const useContextStore = create()( } else { newValidatingIds.delete(id); } - + return { validatingIds: newValidatingIds }; }, false, 'setValidating'); }, - - + + reorderContexts: (startIndex: number, endIndex: number) => { set((state) => { const newContexts = [...state.contexts]; const [removed] = newContexts.splice(startIndex, 1); newContexts.splice(endIndex, 0, removed); - + return { contexts: newContexts }; }, false, 'reorderContexts'); }, - - + + updateContext: (id: string, updates: Partial) => { set((state) => { - const contexts = state.contexts.map(c => + const contexts = state.contexts.map(c => c.id === id ? { ...c, ...updates } as ContextItem : c ); - + return { contexts }; }, false, 'updateContext'); } }), { name: 'bitfun-context-storage', - + storage: { + getItem(name: string) { + try { + return storage.getItem(name); + } catch (e) { + console.error(`Failed to get item from storage ${e}`); + } + }, + setItem(name: string, value: StorageValue) { + try { + return storage.setItem(name, JSON.stringify(value)); + } catch (e) { + console.error(`Failed to set item from storage ${e}`); + } + }, + removeItem(name: string) { + try { + return storage.removeItem(name); + } catch (e) { + console.error(`Failed to remove item from storage ${e}`); + } + } + }, serialize: (state: any) => { return JSON.stringify({ ...state.state, @@ -143,7 +166,7 @@ export const useContextStore = create()( validatingIds: Array.from(state.state.validatingIds) }); }, - + deserialize: (str: string) => { const parsed = JSON.parse(str); return { @@ -155,8 +178,8 @@ export const useContextStore = create()( } }; }, - - partialize: (state: any) => ({ + + partialize: (state: any) => ({ contexts: state.contexts.filter((ctx: any) => ctx.type !== 'image') }) } as any @@ -172,35 +195,35 @@ export const useContextStore = create()( export const selectContexts = (state: ContextState) => state.contexts; export const selectContextCount = (state: ContextState) => state.contexts.length; -export const selectContextById = (id: string) => (state: ContextState) => +export const selectContextById = (id: string) => (state: ContextState) => state.contexts.find(c => c.id === id); -export const selectValidationState = (id: string) => (state: ContextState) => +export const selectValidationState = (id: string) => (state: ContextState) => state.validationStates.get(id); -export const selectIsValidating = (id: string) => (state: ContextState) => +export const selectIsValidating = (id: string) => (state: ContextState) => state.validatingIds.has(id); -export const selectHasInvalidContexts = (state: ContextState) => +export const selectHasInvalidContexts = (state: ContextState) => Array.from(state.validationStates.values()).some(v => !v.valid); - + export const cleanupImageContextsFromStorage = () => { try { const storageKey = 'bitfun-context-storage'; - const stored = localStorage.getItem(storageKey); - + const stored = storage.getItem(storageKey); + if (stored) { const parsed = JSON.parse(stored); - + if (parsed.state && Array.isArray(parsed.state.contexts)) { const imageCount = parsed.state.contexts.filter((ctx: any) => ctx.type === 'image').length; - + if (imageCount > 0) { - + parsed.state.contexts = parsed.state.contexts.filter((ctx: any) => ctx.type !== 'image'); - - - localStorage.setItem(storageKey, JSON.stringify(parsed)); + + + storage.setItem(storageKey, JSON.stringify(parsed)); } } } diff --git a/src/web-ui/src/shared/utils/index.ts b/src/web-ui/src/shared/utils/index.ts index ca4d9cef4..53ebc1d80 100644 --- a/src/web-ui/src/shared/utils/index.ts +++ b/src/web-ui/src/shared/utils/index.ts @@ -13,3 +13,5 @@ export * from './debugProbe'; export * from './configConverter'; export * from './contextGenerator'; export * from './eventManager'; +export * from './storageAdapter'; +export * from './sessionStorageAdapter'; diff --git a/src/web-ui/src/shared/utils/sessionStorageAdapter.ts b/src/web-ui/src/shared/utils/sessionStorageAdapter.ts new file mode 100644 index 000000000..89e35d36c --- /dev/null +++ b/src/web-ui/src/shared/utils/sessionStorageAdapter.ts @@ -0,0 +1,87 @@ +class SessionStorageAdapter { + private isAvailable: boolean; + private memoryStorage: Map = new Map(); + + constructor() { + this.isAvailable = this.checkAvailability(); + } + + private checkAvailability(): boolean { + try { + const testkey = 'testkey'; + sessionStorage.setItem(testkey, testkey); + sessionStorage.removeItem(testkey); + return true; + } catch (e) { + console.error("SessionStorageAdapter check failed for session storage", e); + return false; + } + } + + getItem(key: string): string | null { + if (this.isAvailable) { + try { + return sessionStorage.getItem(key); + } catch (e) { + console.error(`Failed to get ${key} for `, e); + } + } + return this.memoryStorage.get(key) || null; + } + + setItem(key: string, value: string): void { + if (this.isAvailable) { + try { + sessionStorage.setItem(key, value); + this.memoryStorage.set(key, value); + return; + } catch (e) { + console.warn(`Failed to set ${key} Local storage not available`, e); + } + } + this.memoryStorage.set(key, value); + } + + removeItem(key: string): void { + if (this.isAvailable) { + try { + sessionStorage.removeItem(key); + this.memoryStorage.delete(key); + return; + } catch (e) { + console.warn(`Failed to delete ${key} Local storage not available`, e); + } + } + this.memoryStorage.delete(key); + } + + clear(): void { + if (this.isAvailable) { + try { + sessionStorage.clear(); + this.memoryStorage.clear(); + return; + } catch (e) { + console.warn(`Failed to clear storage. Local storage not available`, e); + } + } + this.memoryStorage.clear(); + } + + getKeys(): string[] { + if (this.isAvailable) { + try { + return Object.keys(sessionStorage); + } catch (e) { + console.warn(`Failed to get keys. Local storage not available`, e); + } + } + return Array.from(this.memoryStorage.keys()); + } + + hasKey(key: string): boolean { + return this.getItem(key) !== null; + } +} + +export const sessionStorageAdapter = new SessionStorageAdapter(); \ No newline at end of file diff --git a/src/web-ui/src/shared/utils/storageAdapter.ts b/src/web-ui/src/shared/utils/storageAdapter.ts new file mode 100644 index 000000000..9bb06adf5 --- /dev/null +++ b/src/web-ui/src/shared/utils/storageAdapter.ts @@ -0,0 +1,88 @@ +class StorageAdapter { + private isAvailable: boolean; + private memoryStorage: Map = new Map(); + + constructor() { + this.isAvailable = this.checkAvailability(); + } + + private checkAvailability(): boolean { + try { + const storageKey = '__storageKey__'; + localStorage.setItem(storageKey, storageKey); + localStorage.removeItem(storageKey); + return true; + } catch (e) { + console.error("SessionStorageAdapter check failed for session storage", e); + return false; + } + } + + + getItem(key: string): string | null { + if (this.isAvailable) { + try { + return localStorage.getItem(key); + } catch (e) { + console.error(`Failed to get ${key} for `, e); + } + } + return this.memoryStorage.get(key) || null; + } + + setItem(key: string, value: string): void { + if (this.isAvailable) { + try { + localStorage.setItem(key, value); + this.memoryStorage.set(key, value); + return; + } catch (e) { + console.warn(`Failed to set ${key} Local storage not available`, e); + } + } + this.memoryStorage.set(key, value); + } + + removeItem(key: string): void { + if (this.isAvailable) { + try { + localStorage.removeItem(key); + this.memoryStorage.delete(key); + return; + } catch (e) { + console.warn(`Failed to delete ${key} Local storage not available`, e); + } + } + this.memoryStorage.delete(key); + } + + clear(): void { + if (this.isAvailable) { + try { + localStorage.clear(); + this.memoryStorage.clear(); + return; + } catch (e) { + console.warn(`Failed to clear storage. Local storage not available`, e); + } + } + this.memoryStorage.clear(); + } + + getKeys(): string[] { + if (this.isAvailable) { + try { + return Object.keys(localStorage); + } catch (e) { + console.warn(`Failed to get keys. Local storage not available`, e); + } + } + return Array.from(this.memoryStorage.keys()); + } + + hasKey(key: string): boolean { + return this.getItem(key) !== null; + } +} + +export const storage = new StorageAdapter(); \ No newline at end of file diff --git a/src/web-ui/src/tools/editor/services/EditorManager.ts b/src/web-ui/src/tools/editor/services/EditorManager.ts index 98ec47198..a955f0c3f 100644 --- a/src/web-ui/src/tools/editor/services/EditorManager.ts +++ b/src/web-ui/src/tools/editor/services/EditorManager.ts @@ -13,6 +13,7 @@ import { import { globalEventBus } from '../../../infrastructure/event-bus'; import { getMonacoLanguage } from '@/infrastructure/language-detection'; import { createLogger } from '@/shared/utils/logger'; +import { storage } from '@/shared'; const log = createLogger('EditorManager'); @@ -416,7 +417,7 @@ export class EditorManager implements IEditorManager { private loadConfig(): void { try { - const savedConfig = localStorage.getItem('editor-config'); + const savedConfig = storage.getItem('editor-config'); if (savedConfig) { const parsed = JSON.parse(savedConfig); this.config = { ...DEFAULT_CONFIG, ...parsed }; @@ -428,7 +429,7 @@ export class EditorManager implements IEditorManager { private saveConfig(): void { try { - localStorage.setItem('editor-config', JSON.stringify(this.config)); + storage.setItem('editor-config', JSON.stringify(this.config)); } catch (error) { log.warn('Failed to save config', error); } diff --git a/src/web-ui/src/tools/lsp/services/LspConfigService.ts b/src/web-ui/src/tools/lsp/services/LspConfigService.ts index 87ff426a8..520b26911 100644 --- a/src/web-ui/src/tools/lsp/services/LspConfigService.ts +++ b/src/web-ui/src/tools/lsp/services/LspConfigService.ts @@ -2,6 +2,7 @@ * LSP config service (user-facing settings). */ +import { storage } from '@/shared'; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('LspConfigService'); @@ -30,7 +31,7 @@ class LspConfigService { getSettings(): LspSettings { try { - const saved = localStorage.getItem(LSP_SETTINGS_KEY); + const saved = storage.getItem(LSP_SETTINGS_KEY); if (saved) { const parsed = JSON.parse(saved); return { ...DEFAULT_LSP_SETTINGS, ...parsed }; @@ -43,7 +44,7 @@ class LspConfigService { saveSettings(settings: LspSettings): void { try { - localStorage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); + storage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); } catch (error) { log.error('Failed to save settings', { error }); throw error; diff --git a/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts b/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts index 331d54b22..69d6bb534 100644 --- a/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts +++ b/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts @@ -1,3 +1,4 @@ +import { storage } from '@/shared'; import { STORAGE_KEYS } from '@/shared/constants/app'; import { createLogger } from '@/shared/utils/logger'; @@ -73,7 +74,7 @@ function normalizeState(raw: unknown): ManualTerminalProfilesState { export function loadManualTerminalProfiles(workspacePath: string): ManualTerminalProfilesState { try { - const raw = localStorage.getItem(getStorageKey(workspacePath)); + const raw = storage.getItem(getStorageKey(workspacePath)); if (raw) { return normalizeState(JSON.parse(raw)); } @@ -89,7 +90,7 @@ export function saveManualTerminalProfiles( state: ManualTerminalProfilesState, ): void { try { - localStorage.setItem(getStorageKey(workspacePath), JSON.stringify(normalizeState(state))); + storage.setItem(getStorageKey(workspacePath), JSON.stringify(normalizeState(state))); } catch (error) { logger.error('Failed to save manual terminal profiles', { workspacePath, error }); } diff --git a/src/web-ui/vite.config.ts b/src/web-ui/vite.config.ts index 96015dee5..073312887 100644 --- a/src/web-ui/vite.config.ts +++ b/src/web-ui/vite.config.ts @@ -99,6 +99,7 @@ export default defineConfig(({ mode, command }) => { outDir: '../../dist', // Empty the output directory emptyOutDir: true, + minify: false, } }; }); From 337f346a99a02ecba8fa797570af31e7cfaaf3cf Mon Sep 17 00:00:00 2001 From: Marqle Date: Mon, 27 Apr 2026 16:37:52 +0800 Subject: [PATCH 06/31] compile fix --- src/apps/desktop/Cargo.toml | 2 +- src/apps/desktop/src/lib.rs | 2 +- src/crates/webdriver/Cargo.toml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index ea270acf8..0213818c8 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -76,5 +76,5 @@ windows = { version = "0.61.3", features = [ ] } windows-core = "0.61.2" -[target.'cfg(all(target_os = "linux", not(target_env = "ohos)))'.dependencies] +[target.'cfg(all(target_os = "linux", not(target_env = "ohos")))'.dependencies] leptess = "0.14.0" diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 8a38f4f32..a5974ee77 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -1,7 +1,7 @@ #![allow(non_snake_case)] //! BitFun Desktop - Tauri-based desktop application with TransportAdapter architecture -pub mod api; +// pub mod api; pub mod computer_use; pub mod logging; pub mod macos_menubar; diff --git a/src/crates/webdriver/Cargo.toml b/src/crates/webdriver/Cargo.toml index 9e22dee6a..8b3b869bb 100644 --- a/src/crates/webdriver/Cargo.toml +++ b/src/crates/webdriver/Cargo.toml @@ -30,8 +30,8 @@ webview2-com = "0.38.2" windows = { version = "0.61.3", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Com_StructuredStorage"] } windows-core = "0.61.2" -[target.'cfg(target_os = "linux")'.dependencies] -glib = "0.18.5" -gtk = "0.18.2" -tempfile = "3" -webkit2gtk = "2.0.2" +# [target.'cfg(target_os = "linux")'.dependencies] +# glib = "0.18.5" +# gtk = "0.18.2" +# tempfile = "3" +# webkit2gtk = "2.0.2" From 3fd350069850bfcbb47a6434abe6bddb353d5926 Mon Sep 17 00:00:00 2001 From: Marqle Date: Mon, 27 Apr 2026 16:37:52 +0800 Subject: [PATCH 07/31] compile fix --- src/web-ui/src/flow_chat/components/ChatInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 74780f51c..bfab815db 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -813,7 +813,7 @@ export const ChatInput: React.FC = ({ }); dispatchMode({ type: 'SET_CURRENT_MODE', payload: nextMode }); try { - sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', session.mode); + sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', nextMode); } catch { // ignore } From e395caffd4aab337e3f8639b0bfd006b1648919a Mon Sep 17 00:00:00 2001 From: Marqle Date: Mon, 27 Apr 2026 17:13:28 +0800 Subject: [PATCH 08/31] move vcoder to apps --- {app/oh/Vcoder => src/apps/vcoder}/.gitignore | 0 .../Vcoder => src/apps/vcoder}/AppScope/app.json5 | 2 +- .../AppScope/resources/base/element/string.json | 2 +- .../AppScope/resources/base/media/background.png | Bin .../AppScope/resources/base/media/foreground.png | Bin .../resources/base/media/layered_image.json | 0 .../Vcoder => src/apps/vcoder}/build-profile.json5 | 10 +++++----- .../oh/Vcoder => src/apps/vcoder}/code-linter.json5 | 0 {app/oh/Vcoder => src/apps/vcoder}/entry/.gitignore | 0 .../apps/vcoder}/entry/build-profile.json5 | 0 .../Vcoder => src/apps/vcoder}/entry/hvigorfile.ts | 0 .../apps/vcoder}/entry/obfuscation-rules.txt | 0 .../apps/vcoder}/entry/oh-package-lock.json5 | 0 .../apps/vcoder}/entry/oh-package.json5 | 0 .../apps/vcoder}/entry/src/main/cpp/CMakeLists.txt | 0 .../apps/vcoder}/entry/src/main/cpp/napi_init.cpp | 0 .../main/cpp/types/libbitfun_desktop_lib/Index.d.ts | 0 .../types/libbitfun_desktop_lib/oh-package.json5 | 0 .../entry/src/main/cpp/types/libentry/Index.d.ts | 0 .../src/main/cpp/types/libentry/oh-package.json5 | 0 .../src/main/ets/entryability/EntryAbility.ets | 0 .../ets/entrybackupability/EntryBackupAbility.ets | 0 .../apps/vcoder}/entry/src/main/ets/pages/Index.ets | 0 .../src/main/ets/utils/CommonEventListener.ets | 0 .../entry/src/main/ets/utils/CommonUtils.ets | 0 .../entry/src/main/ets/utils/DevecoStart.ets | 0 .../vcoder}/entry/src/main/ets/utils/Result.ets | 0 .../apps/vcoder}/entry/src/main/module.json5 | 0 .../src/main/resources/base/element/color.json | 0 .../src/main/resources/base/element/float.json | 0 .../src/main/resources/base/element/string.json | 0 .../src/main/resources/base/media/background.png | Bin .../src/main/resources/base/media/foreground.png | Bin .../main/resources/base/media/layered_image.json | 0 .../src/main/resources/base/media/startIcon.png | Bin .../main/resources/base/profile/backup_config.json | 0 .../src/main/resources/base/profile/main_pages.json | 0 .../src/main/resources/dark/element/color.json | 0 .../apps/vcoder}/entry/src/mock/mock-config.json5 | 0 .../entry/src/ohosTest/ets/test/Ability.test.ets | 0 .../entry/src/ohosTest/ets/test/List.test.ets | 0 .../apps/vcoder}/entry/src/ohosTest/module.json5 | 0 .../apps/vcoder}/entry/src/test/List.test.ets | 0 .../apps/vcoder}/entry/src/test/LocalUnit.test.ets | 0 .../apps/vcoder}/hvigor/hvigor-config.json5 | 0 {app/oh/Vcoder => src/apps/vcoder}/hvigorfile.ts | 0 .../apps/vcoder}/oh-package-lock.json5 | 0 {app/oh/Vcoder => src/apps/vcoder}/oh-package.json5 | 0 48 files changed, 7 insertions(+), 7 deletions(-) rename {app/oh/Vcoder => src/apps/vcoder}/.gitignore (100%) rename {app/oh/Vcoder => src/apps/vcoder}/AppScope/app.json5 (83%) rename {app/oh/Vcoder => src/apps/vcoder}/AppScope/resources/base/element/string.json (71%) rename {app/oh/Vcoder => src/apps/vcoder}/AppScope/resources/base/media/background.png (100%) rename {app/oh/Vcoder => src/apps/vcoder}/AppScope/resources/base/media/foreground.png (100%) rename {app/oh/Vcoder => src/apps/vcoder}/AppScope/resources/base/media/layered_image.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/build-profile.json5 (73%) rename {app/oh/Vcoder => src/apps/vcoder}/code-linter.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/.gitignore (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/build-profile.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/hvigorfile.ts (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/obfuscation-rules.txt (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/oh-package-lock.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/oh-package.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/cpp/CMakeLists.txt (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/cpp/napi_init.cpp (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/cpp/types/libentry/Index.d.ts (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/cpp/types/libentry/oh-package.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/ets/entryability/EntryAbility.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/ets/pages/Index.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/ets/utils/CommonEventListener.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/ets/utils/CommonUtils.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/ets/utils/DevecoStart.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/ets/utils/Result.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/module.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/element/color.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/element/float.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/element/string.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/media/background.png (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/media/foreground.png (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/media/layered_image.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/media/startIcon.png (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/profile/backup_config.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/base/profile/main_pages.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/main/resources/dark/element/color.json (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/mock/mock-config.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/ohosTest/ets/test/Ability.test.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/ohosTest/ets/test/List.test.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/ohosTest/module.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/test/List.test.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/entry/src/test/LocalUnit.test.ets (100%) rename {app/oh/Vcoder => src/apps/vcoder}/hvigor/hvigor-config.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/hvigorfile.ts (100%) rename {app/oh/Vcoder => src/apps/vcoder}/oh-package-lock.json5 (100%) rename {app/oh/Vcoder => src/apps/vcoder}/oh-package.json5 (100%) diff --git a/app/oh/Vcoder/.gitignore b/src/apps/vcoder/.gitignore similarity index 100% rename from app/oh/Vcoder/.gitignore rename to src/apps/vcoder/.gitignore diff --git a/app/oh/Vcoder/AppScope/app.json5 b/src/apps/vcoder/AppScope/app.json5 similarity index 83% rename from app/oh/Vcoder/AppScope/app.json5 rename to src/apps/vcoder/AppScope/app.json5 index 61c2b0373..7b14e5ef5 100644 --- a/app/oh/Vcoder/AppScope/app.json5 +++ b/src/apps/vcoder/AppScope/app.json5 @@ -1,6 +1,6 @@ { "app": { - "bundleName": "com.huawei.vcoder", + "bundleName": "com.huawei.BitFun", "vendor": "example", "versionCode": 1000000, "versionName": "1.0.0", diff --git a/app/oh/Vcoder/AppScope/resources/base/element/string.json b/src/apps/vcoder/AppScope/resources/base/element/string.json similarity index 71% rename from app/oh/Vcoder/AppScope/resources/base/element/string.json rename to src/apps/vcoder/AppScope/resources/base/element/string.json index 7162e0a96..9678373bb 100644 --- a/app/oh/Vcoder/AppScope/resources/base/element/string.json +++ b/src/apps/vcoder/AppScope/resources/base/element/string.json @@ -2,7 +2,7 @@ "string": [ { "name": "app_name", - "value": "Vcoder" + "value": "BitFun" } ] } diff --git a/app/oh/Vcoder/AppScope/resources/base/media/background.png b/src/apps/vcoder/AppScope/resources/base/media/background.png similarity index 100% rename from app/oh/Vcoder/AppScope/resources/base/media/background.png rename to src/apps/vcoder/AppScope/resources/base/media/background.png diff --git a/app/oh/Vcoder/AppScope/resources/base/media/foreground.png b/src/apps/vcoder/AppScope/resources/base/media/foreground.png similarity index 100% rename from app/oh/Vcoder/AppScope/resources/base/media/foreground.png rename to src/apps/vcoder/AppScope/resources/base/media/foreground.png diff --git a/app/oh/Vcoder/AppScope/resources/base/media/layered_image.json b/src/apps/vcoder/AppScope/resources/base/media/layered_image.json similarity index 100% rename from app/oh/Vcoder/AppScope/resources/base/media/layered_image.json rename to src/apps/vcoder/AppScope/resources/base/media/layered_image.json diff --git a/app/oh/Vcoder/build-profile.json5 b/src/apps/vcoder/build-profile.json5 similarity index 73% rename from app/oh/Vcoder/build-profile.json5 rename to src/apps/vcoder/build-profile.json5 index c792291de..074b9ce93 100644 --- a/app/oh/Vcoder/build-profile.json5 +++ b/src/apps/vcoder/build-profile.json5 @@ -5,13 +5,13 @@ "name": "default", "type": "HarmonyOS", "material": { - "certpath": "C:\\Users\\lijiale\\.ohos\\config\\default_Vcoder_imQPBwlDhZKHpu-LkgPseVnUO1DIbVYZGHwovJ97cvY=.cer", + "certpath": "C:\\Users\\lijiale\\.ohos\\config\\default_vcoder_mLoJ04E4ZMVs3NKJi5Yg30Rswoz227jnCV-I8k6sAvc=.cer", "keyAlias": "debugKey", - "keyPassword": "0000001B63A5404F339099984BDD616073E604D447C289B47D5DA41028E43A2E5D4DF2BE67AA24F4CD3464", - "profile": "C:\\Users\\lijiale\\.ohos\\config\\default_Vcoder_imQPBwlDhZKHpu-LkgPseVnUO1DIbVYZGHwovJ97cvY=.p7b", + "keyPassword": "0000001B1AC4E38D3159F409BFB38FF2B667CE1FE6E108A4C111EF2A0165222070DE0DE0656D86DB91D77C", + "profile": "C:\\Users\\lijiale\\.ohos\\config\\default_vcoder_mLoJ04E4ZMVs3NKJi5Yg30Rswoz227jnCV-I8k6sAvc=.p7b", "signAlg": "SHA256withECDSA", - "storeFile": "C:\\Users\\lijiale\\.ohos\\config\\default_Vcoder_imQPBwlDhZKHpu-LkgPseVnUO1DIbVYZGHwovJ97cvY=.p12", - "storePassword": "0000001B8D25281252401BEC116357BEDCA9ABEF740814063184A05219DA4399AB405785E0DFC726A24DA1" + "storeFile": "C:\\Users\\lijiale\\.ohos\\config\\default_vcoder_mLoJ04E4ZMVs3NKJi5Yg30Rswoz227jnCV-I8k6sAvc=.p12", + "storePassword": "0000001BF771363E57B97E663BB05D7EF3F6EA63ED750CF7C726306B8714BD0DF072E44D384B69C6FE4F64" } } ], diff --git a/app/oh/Vcoder/code-linter.json5 b/src/apps/vcoder/code-linter.json5 similarity index 100% rename from app/oh/Vcoder/code-linter.json5 rename to src/apps/vcoder/code-linter.json5 diff --git a/app/oh/Vcoder/entry/.gitignore b/src/apps/vcoder/entry/.gitignore similarity index 100% rename from app/oh/Vcoder/entry/.gitignore rename to src/apps/vcoder/entry/.gitignore diff --git a/app/oh/Vcoder/entry/build-profile.json5 b/src/apps/vcoder/entry/build-profile.json5 similarity index 100% rename from app/oh/Vcoder/entry/build-profile.json5 rename to src/apps/vcoder/entry/build-profile.json5 diff --git a/app/oh/Vcoder/entry/hvigorfile.ts b/src/apps/vcoder/entry/hvigorfile.ts similarity index 100% rename from app/oh/Vcoder/entry/hvigorfile.ts rename to src/apps/vcoder/entry/hvigorfile.ts diff --git a/app/oh/Vcoder/entry/obfuscation-rules.txt b/src/apps/vcoder/entry/obfuscation-rules.txt similarity index 100% rename from app/oh/Vcoder/entry/obfuscation-rules.txt rename to src/apps/vcoder/entry/obfuscation-rules.txt diff --git a/app/oh/Vcoder/entry/oh-package-lock.json5 b/src/apps/vcoder/entry/oh-package-lock.json5 similarity index 100% rename from app/oh/Vcoder/entry/oh-package-lock.json5 rename to src/apps/vcoder/entry/oh-package-lock.json5 diff --git a/app/oh/Vcoder/entry/oh-package.json5 b/src/apps/vcoder/entry/oh-package.json5 similarity index 100% rename from app/oh/Vcoder/entry/oh-package.json5 rename to src/apps/vcoder/entry/oh-package.json5 diff --git a/app/oh/Vcoder/entry/src/main/cpp/CMakeLists.txt b/src/apps/vcoder/entry/src/main/cpp/CMakeLists.txt similarity index 100% rename from app/oh/Vcoder/entry/src/main/cpp/CMakeLists.txt rename to src/apps/vcoder/entry/src/main/cpp/CMakeLists.txt diff --git a/app/oh/Vcoder/entry/src/main/cpp/napi_init.cpp b/src/apps/vcoder/entry/src/main/cpp/napi_init.cpp similarity index 100% rename from app/oh/Vcoder/entry/src/main/cpp/napi_init.cpp rename to src/apps/vcoder/entry/src/main/cpp/napi_init.cpp diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts b/src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts similarity index 100% rename from app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts rename to src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 b/src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 similarity index 100% rename from app/oh/Vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 rename to src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libentry/Index.d.ts b/src/apps/vcoder/entry/src/main/cpp/types/libentry/Index.d.ts similarity index 100% rename from app/oh/Vcoder/entry/src/main/cpp/types/libentry/Index.d.ts rename to src/apps/vcoder/entry/src/main/cpp/types/libentry/Index.d.ts diff --git a/app/oh/Vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 b/src/apps/vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 similarity index 100% rename from app/oh/Vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 rename to src/apps/vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 diff --git a/app/oh/Vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets similarity index 100% rename from app/oh/Vcoder/entry/src/main/ets/entryability/EntryAbility.ets rename to src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets diff --git a/app/oh/Vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/src/apps/vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets similarity index 100% rename from app/oh/Vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets rename to src/apps/vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets diff --git a/app/oh/Vcoder/entry/src/main/ets/pages/Index.ets b/src/apps/vcoder/entry/src/main/ets/pages/Index.ets similarity index 100% rename from app/oh/Vcoder/entry/src/main/ets/pages/Index.ets rename to src/apps/vcoder/entry/src/main/ets/pages/Index.ets diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/CommonEventListener.ets b/src/apps/vcoder/entry/src/main/ets/utils/CommonEventListener.ets similarity index 100% rename from app/oh/Vcoder/entry/src/main/ets/utils/CommonEventListener.ets rename to src/apps/vcoder/entry/src/main/ets/utils/CommonEventListener.ets diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/CommonUtils.ets b/src/apps/vcoder/entry/src/main/ets/utils/CommonUtils.ets similarity index 100% rename from app/oh/Vcoder/entry/src/main/ets/utils/CommonUtils.ets rename to src/apps/vcoder/entry/src/main/ets/utils/CommonUtils.ets diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/DevecoStart.ets b/src/apps/vcoder/entry/src/main/ets/utils/DevecoStart.ets similarity index 100% rename from app/oh/Vcoder/entry/src/main/ets/utils/DevecoStart.ets rename to src/apps/vcoder/entry/src/main/ets/utils/DevecoStart.ets diff --git a/app/oh/Vcoder/entry/src/main/ets/utils/Result.ets b/src/apps/vcoder/entry/src/main/ets/utils/Result.ets similarity index 100% rename from app/oh/Vcoder/entry/src/main/ets/utils/Result.ets rename to src/apps/vcoder/entry/src/main/ets/utils/Result.ets diff --git a/app/oh/Vcoder/entry/src/main/module.json5 b/src/apps/vcoder/entry/src/main/module.json5 similarity index 100% rename from app/oh/Vcoder/entry/src/main/module.json5 rename to src/apps/vcoder/entry/src/main/module.json5 diff --git a/app/oh/Vcoder/entry/src/main/resources/base/element/color.json b/src/apps/vcoder/entry/src/main/resources/base/element/color.json similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/element/color.json rename to src/apps/vcoder/entry/src/main/resources/base/element/color.json diff --git a/app/oh/Vcoder/entry/src/main/resources/base/element/float.json b/src/apps/vcoder/entry/src/main/resources/base/element/float.json similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/element/float.json rename to src/apps/vcoder/entry/src/main/resources/base/element/float.json diff --git a/app/oh/Vcoder/entry/src/main/resources/base/element/string.json b/src/apps/vcoder/entry/src/main/resources/base/element/string.json similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/element/string.json rename to src/apps/vcoder/entry/src/main/resources/base/element/string.json diff --git a/app/oh/Vcoder/entry/src/main/resources/base/media/background.png b/src/apps/vcoder/entry/src/main/resources/base/media/background.png similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/media/background.png rename to src/apps/vcoder/entry/src/main/resources/base/media/background.png diff --git a/app/oh/Vcoder/entry/src/main/resources/base/media/foreground.png b/src/apps/vcoder/entry/src/main/resources/base/media/foreground.png similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/media/foreground.png rename to src/apps/vcoder/entry/src/main/resources/base/media/foreground.png diff --git a/app/oh/Vcoder/entry/src/main/resources/base/media/layered_image.json b/src/apps/vcoder/entry/src/main/resources/base/media/layered_image.json similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/media/layered_image.json rename to src/apps/vcoder/entry/src/main/resources/base/media/layered_image.json diff --git a/app/oh/Vcoder/entry/src/main/resources/base/media/startIcon.png b/src/apps/vcoder/entry/src/main/resources/base/media/startIcon.png similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/media/startIcon.png rename to src/apps/vcoder/entry/src/main/resources/base/media/startIcon.png diff --git a/app/oh/Vcoder/entry/src/main/resources/base/profile/backup_config.json b/src/apps/vcoder/entry/src/main/resources/base/profile/backup_config.json similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/profile/backup_config.json rename to src/apps/vcoder/entry/src/main/resources/base/profile/backup_config.json diff --git a/app/oh/Vcoder/entry/src/main/resources/base/profile/main_pages.json b/src/apps/vcoder/entry/src/main/resources/base/profile/main_pages.json similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/base/profile/main_pages.json rename to src/apps/vcoder/entry/src/main/resources/base/profile/main_pages.json diff --git a/app/oh/Vcoder/entry/src/main/resources/dark/element/color.json b/src/apps/vcoder/entry/src/main/resources/dark/element/color.json similarity index 100% rename from app/oh/Vcoder/entry/src/main/resources/dark/element/color.json rename to src/apps/vcoder/entry/src/main/resources/dark/element/color.json diff --git a/app/oh/Vcoder/entry/src/mock/mock-config.json5 b/src/apps/vcoder/entry/src/mock/mock-config.json5 similarity index 100% rename from app/oh/Vcoder/entry/src/mock/mock-config.json5 rename to src/apps/vcoder/entry/src/mock/mock-config.json5 diff --git a/app/oh/Vcoder/entry/src/ohosTest/ets/test/Ability.test.ets b/src/apps/vcoder/entry/src/ohosTest/ets/test/Ability.test.ets similarity index 100% rename from app/oh/Vcoder/entry/src/ohosTest/ets/test/Ability.test.ets rename to src/apps/vcoder/entry/src/ohosTest/ets/test/Ability.test.ets diff --git a/app/oh/Vcoder/entry/src/ohosTest/ets/test/List.test.ets b/src/apps/vcoder/entry/src/ohosTest/ets/test/List.test.ets similarity index 100% rename from app/oh/Vcoder/entry/src/ohosTest/ets/test/List.test.ets rename to src/apps/vcoder/entry/src/ohosTest/ets/test/List.test.ets diff --git a/app/oh/Vcoder/entry/src/ohosTest/module.json5 b/src/apps/vcoder/entry/src/ohosTest/module.json5 similarity index 100% rename from app/oh/Vcoder/entry/src/ohosTest/module.json5 rename to src/apps/vcoder/entry/src/ohosTest/module.json5 diff --git a/app/oh/Vcoder/entry/src/test/List.test.ets b/src/apps/vcoder/entry/src/test/List.test.ets similarity index 100% rename from app/oh/Vcoder/entry/src/test/List.test.ets rename to src/apps/vcoder/entry/src/test/List.test.ets diff --git a/app/oh/Vcoder/entry/src/test/LocalUnit.test.ets b/src/apps/vcoder/entry/src/test/LocalUnit.test.ets similarity index 100% rename from app/oh/Vcoder/entry/src/test/LocalUnit.test.ets rename to src/apps/vcoder/entry/src/test/LocalUnit.test.ets diff --git a/app/oh/Vcoder/hvigor/hvigor-config.json5 b/src/apps/vcoder/hvigor/hvigor-config.json5 similarity index 100% rename from app/oh/Vcoder/hvigor/hvigor-config.json5 rename to src/apps/vcoder/hvigor/hvigor-config.json5 diff --git a/app/oh/Vcoder/hvigorfile.ts b/src/apps/vcoder/hvigorfile.ts similarity index 100% rename from app/oh/Vcoder/hvigorfile.ts rename to src/apps/vcoder/hvigorfile.ts diff --git a/app/oh/Vcoder/oh-package-lock.json5 b/src/apps/vcoder/oh-package-lock.json5 similarity index 100% rename from app/oh/Vcoder/oh-package-lock.json5 rename to src/apps/vcoder/oh-package-lock.json5 diff --git a/app/oh/Vcoder/oh-package.json5 b/src/apps/vcoder/oh-package.json5 similarity index 100% rename from app/oh/Vcoder/oh-package.json5 rename to src/apps/vcoder/oh-package.json5 From c07d975c0229e554467ecccbb3fcaabf8b9e6601 Mon Sep 17 00:00:00 2001 From: Marqle Date: Tue, 28 Apr 2026 10:00:17 +0800 Subject: [PATCH 09/31] compile fix --- src/apps/desktop/capabilities/default.json | 23 +--- src/apps/desktop/src/api/computer_use_api.rs | 31 +---- .../desktop/src/computer_use/desktop_host.rs | 120 ++---------------- .../desktop/src/computer_use/screen_ocr.rs | 5 - src/apps/desktop/src/lib.rs | 62 +-------- src/apps/desktop/src/theme.rs | 17 --- src/crates/core/Cargo.toml | 4 +- 7 files changed, 18 insertions(+), 244 deletions(-) diff --git a/src/apps/desktop/capabilities/default.json b/src/apps/desktop/capabilities/default.json index 0f22e9753..ab26713ec 100644 --- a/src/apps/desktop/capabilities/default.json +++ b/src/apps/desktop/capabilities/default.json @@ -43,21 +43,9 @@ "core:window:allow-unmaximize", "core:window:allow-unminimize", "core:window:allow-set-min-size", - "dialog:default", - "dialog:allow-open", - "dialog:allow-save", - "dialog:allow-ask", - "dialog:allow-confirm", - "dialog:allow-message", "opener:default", "opener:allow-open-url", - { - "identifier": "opener:allow-open-path", - "allow": [ - { "path": "$APPDATA/**" }, - { "path": "$HOME/**" } - ] - }, + "opener:allow-open-path", "opener:allow-reveal-item-in-dir", "fs:default", "fs:allow-read-file", @@ -96,13 +84,6 @@ "allow": [ { "path": "$HOME/**" } ] - }, - "notification:default", - "notification:allow-notify", - "notification:allow-show", - "notification:allow-request-permission", - "notification:allow-check-permissions", - "notification:allow-permission-state", - "notification:allow-is-permission-granted" + } ] } \ No newline at end of file diff --git a/src/apps/desktop/src/api/computer_use_api.rs b/src/apps/desktop/src/api/computer_use_api.rs index ae84c1773..50d4d827f 100644 --- a/src/apps/desktop/src/api/computer_use_api.rs +++ b/src/apps/desktop/src/api/computer_use_api.rs @@ -1,9 +1,6 @@ //! Tauri commands for Computer use (permissions + settings deep links). use crate::api::app_state::AppState; -use crate::computer_use::DesktopComputerUseHost; -use bitfun_core::agentic::tools::computer_use_host::ComputerUseHost; -use bitfun_core::service::config::types::AIConfig; use serde::{Deserialize, Serialize}; use tauri::State; @@ -27,36 +24,12 @@ pub struct ComputerUseOpenSettingsRequest { pub async fn computer_use_get_status( state: State<'_, AppState>, ) -> Result { - let ai: AIConfig = state - .config_service - .get_config(Some("ai")) - .await - .map_err(|e| e.to_string())?; - - let host = DesktopComputerUseHost::new(); - let snap = host - .permission_snapshot() - .await - .map_err(|e| e.to_string())?; - - Ok(ComputerUseStatusResponse { - computer_use_enabled: ai.computer_use_enabled, - accessibility_granted: snap.accessibility_granted, - screen_capture_granted: snap.screen_capture_granted, - platform_note: snap.platform_note, - }) + Err("computer_use_get_status error".to_string()) } #[tauri::command] pub async fn computer_use_request_permissions() -> Result<(), String> { - let host = DesktopComputerUseHost::new(); - host.request_accessibility_permission() - .await - .map_err(|e| e.to_string())?; - host.request_screen_capture_permission() - .await - .map_err(|e| e.to_string())?; - Ok(()) + Err("computer_use_request_permissions error".to_string()) } #[tauri::command] diff --git a/src/apps/desktop/src/computer_use/desktop_host.rs b/src/apps/desktop/src/computer_use/desktop_host.rs index 7477f6d1a..6141fa552 100644 --- a/src/apps/desktop/src/computer_use/desktop_host.rs +++ b/src/apps/desktop/src/computer_use/desktop_host.rs @@ -3,17 +3,16 @@ use anyhow::anyhow; use async_trait::async_trait; use bitfun_core::agentic::tools::computer_use_host::{ - clamp_point_crop_half_extent, ActionRecord, AppClickParams, AppInfo, AppSelector, - AppStateSnapshot, AppWaitPredicate, ClickTarget, ComputerScreenshot, ComputerUseDisplayInfo, + clamp_point_crop_half_extent, ActionRecord, AppSelector, + AppStateSnapshot, ClickTarget, ComputerScreenshot, ComputerUseDisplayInfo, ComputerUseHost, ComputerUseImageContentRect, ComputerUseImageGlobalBounds, ComputerUseImplicitScreenshotCenter, ComputerUseInteractionScreenshotKind, ComputerUseInteractionState, ComputerUseLastMutationKind, ComputerUseNavigateQuadrant, ComputerUseNavigationRect, ComputerUsePermissionSnapshot, ComputerUseScreenshotParams, - ComputerUseScreenshotRefinement, ComputerUseSessionSnapshot, InteractiveActionResult, - InteractiveClickParams, InteractiveScrollParams, InteractiveTypeTextParams, InteractiveView, - InteractiveViewOpts, LoopDetectionResult, OcrRegionNative, ScreenshotCropCenter, - UiElementLocateQuery, UiElementLocateResult, VisualActionResult, VisualClickParams, VisualMark, - VisualMarkView, VisualMarkViewOpts, COMPUTER_USE_QUADRANT_CLICK_READY_MAX_LONG_EDGE, + ComputerUseScreenshotRefinement, ComputerUseSessionSnapshot, + LoopDetectionResult, OcrRegionNative, ScreenshotCropCenter, + UiElementLocateQuery, UiElementLocateResult, VisualMark, + COMPUTER_USE_QUADRANT_CLICK_READY_MAX_LONG_EDGE, COMPUTER_USE_QUADRANT_EDGE_EXPAND_PX, }; #[cfg(any(target_os = "macos", target_os = "windows"))] @@ -24,11 +23,9 @@ use bitfun_core::agentic::tools::computer_use_optimizer::ComputerUseOptimizer; use bitfun_core::util::errors::{BitFunError, BitFunResult}; use image::codecs::jpeg::JpegEncoder; use image::{DynamicImage, Rgb, RgbImage}; -use log::{debug, info, warn}; +use log::{debug, warn}; use resvg::tiny_skia::{Pixmap, Transform}; use resvg::usvg; -use screenshots::display_info::DisplayInfo; -use screenshots::Screen; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; @@ -1069,8 +1066,8 @@ end tell"#]) Err(BitFunError::Tool(format!("Unknown mouse button: {}", s))) } - fn map_key(name: &str) -> BitFunResult { - Err(BitFunError::Tool(format!("Unknown key name: {}", s))) + fn map_key(name: &str) -> BitFunResult<()> { + Err(BitFunError::Tool(format!("Unknown key name: {}", name))) } fn encode_jpeg(rgb: &RgbImage, quality: u8) -> BitFunResult> { @@ -1629,104 +1626,6 @@ end tell"#]) (0.0, 0.0) } - /// Resolve a screen capture from cache (if still valid and same screen) or capture fresh. - /// - /// Phase 2 fix: when the model has called `desktop.focus_display`, we - /// commit to that screen instead of trusting the mouse pointer. This is - /// the explicit fix for the user's original complaint — on multi-monitor - /// setups the cursor often lives on a different screen than the one the - /// user is reasoning about (e.g. focus is on the laptop screen, mouse - /// is parked on the secondary monitor) and the legacy "screen at mouse - /// pointer" heuristic captured the wrong display. - fn resolve_screenshot_capture( - cached: Option, - mouse_x: f64, - mouse_y: f64, - preferred_display_id: Option, - ) -> BitFunResult<(image::RgbaImage, Screen)> { - let mx = mouse_x.round() as i32; - let my = mouse_y.round() as i32; - let target_display_id = preferred_display_id - .or_else(|| Screen::from_point(mx, my).ok().map(|s| s.display_info.id)); - - if let Some(cache) = cached { - let screen_id_match = Some(cache.screen.display_info.id) == target_display_id; - if cache.capture_time.elapsed() < Duration::from_millis(SCREENSHOT_CACHE_TTL_MS) - && screen_id_match - { - debug!( - "Using cached screenshot (age: {}ms)", - cache.capture_time.elapsed().as_millis() - ); - return Ok((cache.rgba, cache.screen)); - } - } - - let screen = if let Some(id) = preferred_display_id { - Self::find_screen_by_id(id) - .or_else(|| Screen::from_point(mx, my).ok()) - .or_else(|| Screen::from_point(0, 0).ok()) - .ok_or_else(|| { - BitFunError::tool("Screen capture init: no display available".to_string()) - })? - } else { - Screen::from_point(mx, my) - .or_else(|_| Screen::from_point(0, 0)) - .map_err(|e| BitFunError::tool(format!("Screen capture init: {}", e)))? - }; - let rgba = screen.capture().map_err(|e| { - BitFunError::tool(format!( - "Screenshot failed (on macOS grant Screen Recording for BitFun): {}", - e - )) - })?; - Ok((rgba, screen)) - } - - /// Find a [`Screen`] by its display id from the host's enumeration. - fn find_screen_by_id(display_id: u32) -> Option { - Screen::all() - .ok() - .and_then(|all| all.into_iter().find(|s| s.display_info.id == display_id)) - } - - /// Snapshot of all attached displays, with `is_active` / `has_pointer` - /// flags resolved relative to `preferred_display_id` and the current - /// mouse position. - fn enumerate_displays( - preferred_display_id: Option, - mouse_x: f64, - mouse_y: f64, - ) -> Vec { - let mx = mouse_x.round() as i32; - let my = mouse_y.round() as i32; - let pointer_display_id = Screen::from_point(mx, my).ok().map(|s| s.display_info.id); - let active_id = preferred_display_id.or(pointer_display_id); - - let screens = match Screen::all() { - Ok(v) => v, - Err(_) => return vec![], - }; - screens - .into_iter() - .map(|s| { - let d = s.display_info; - ComputerUseDisplayInfo { - display_id: d.id, - is_primary: d.is_primary, - is_active: Some(d.id) == active_id, - has_pointer: Some(d.id) == pointer_display_id, - origin_x: d.x, - origin_y: d.y, - width_logical: d.width, - height_logical: d.height, - scale_factor: d.scale_factor, - foreground_app: None, - } - }) - .collect() - } - fn chord_includes_return_or_enter(keys: &[String]) -> bool { keys.iter() .any(|s| matches!(s.to_lowercase().as_str(), "return" | "enter" | "kp_enter")) @@ -2239,7 +2138,6 @@ impl ComputerUseHost for DesktopComputerUseHost { }; let (mouse_x, mouse_y) = Self::current_mouse_position(); - let displays = Self::enumerate_displays(preferred_display_id, mouse_x, mouse_y); let active_display_id = None; let (click_ready, screenshot_kind, mut recommended_next_action) = diff --git a/src/apps/desktop/src/computer_use/screen_ocr.rs b/src/apps/desktop/src/computer_use/screen_ocr.rs index b377ea10a..456848980 100644 --- a/src/apps/desktop/src/computer_use/screen_ocr.rs +++ b/src/apps/desktop/src/computer_use/screen_ocr.rs @@ -40,11 +40,6 @@ pub fn find_text_matches( return windows_backend::find_text_matches(shot, &query); } - #[cfg(target_os = "linux")] - { - return linux_backend::find_text_matches(shot, &query); - } - #[allow(unreachable_code)] Err(BitFunError::tool( "move_to_text OCR is not supported on this platform.".to_string(), diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index a5974ee77..32c10847f 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -1,20 +1,17 @@ #![allow(non_snake_case)] //! BitFun Desktop - Tauri-based desktop application with TransportAdapter architecture -// pub mod api; -pub mod computer_use; +pub mod api; pub mod logging; pub mod macos_menubar; pub mod theme; use bitfun_core::agentic::tools::computer_use_capability::set_computer_use_desktop_available; -use bitfun_core::agentic::tools::computer_use_host::ComputerUseHostRef; use bitfun_core::infrastructure::ai::AIClientFactory; use bitfun_core::infrastructure::{get_path_manager_arc, try_get_path_manager_arc}; use bitfun_core::service::workspace::get_global_workspace_service; use bitfun_core::util::{elapsed_ms, TimingCollector}; use bitfun_transport::{TauriTransportAdapter, TransportAdapter}; -use serde::Deserialize; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -74,7 +71,7 @@ pub struct OhosPlatform { pub feature: Vec, } -impl Defaut for OhosPlatform { +impl Default for OhosPlatform { fn default() -> Self { Self { version: "6.0.0".to_string(), @@ -93,7 +90,7 @@ impl Defaut for OhosPlatform { /// Tauri application entry point #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let runtime = tokio::runtime::Builder::new_multi_thread + let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(16) .enable_all() .build() @@ -186,19 +183,10 @@ pub async fn _run() { let path_manager = get_path_manager_arc(); - // setup_panic_hook(); - let run_result = tauri::Builder::default() .plugin(logging::build_log_plugin(log_targets)) .plugin(tauri_plugin_opener::init()) - // .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) - // .plugin( - // tauri_plugin_autostart::Builder::new() - // .app_name("BitFun") - // .build(), - // ) - // .plugin(tauri_plugin_notification::init()) .manage(app_state) .manage(coordinator_state) .manage(scheduler_state) @@ -381,7 +369,6 @@ pub async fn _run() { api::agentic_api::set_subagent_timeout, api::agentic_api::delete_session, api::agentic_api::restore_session, - webdriver_bridge_result, api::agentic_api::list_sessions, api::agentic_api::confirm_tool_execution, api::agentic_api::reject_tool_execution, @@ -925,49 +912,6 @@ fn init_mcp_servers(app_handle: tauri::AppHandle) { }); } -fn setup_panic_hook() { - std::panic::set_hook(Box::new(move |panic_info| { - let location = panic_info - .location() - .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) - .unwrap_or_else(|| "unknown location".to_string()); - - let message = panic_info - .payload() - .downcast_ref::<&str>() - .copied() - .or_else(|| { - panic_info - .payload() - .downcast_ref::() - .map(String::as_str) - }) - .unwrap_or("unknown panic message"); - - log::error!("Application panic at {}: {}", location, message); - - // Known wry bug: WKWebView.URL() returns nil after navigating to an - // invalid address, causing url_from_webview to panic on unwrap(). - // This is non-fatal — the webview is still alive — so we log and - // continue instead of killing the process. - // See: https://github.com/tauri-apps/wry/pull/1554 - if location.contains("wry") && location.contains("wkwebview") { - log::warn!("Suppressed non-fatal wry/wkwebview panic, application continues"); - return; - } - - if message.contains("WSAStartup") || message.contains("10093") || message.contains("hyper") - { - log::error!("Network-related crash detected, possible solutions:"); - log::error!(" 1) Restart the application"); - log::error!(" 2) Check Windows network service status"); - log::error!(" 3) Run as administrator"); - } - - std::process::exit(1); - })); -} - fn start_event_loop_with_transport( event_queue: Arc, event_router: Arc, diff --git a/src/apps/desktop/src/theme.rs b/src/apps/desktop/src/theme.rs index 64f1bdcf9..8a14383ec 100644 --- a/src/apps/desktop/src/theme.rs +++ b/src/apps/desktop/src/theme.rs @@ -287,22 +287,5 @@ pub fn create_main_window(app_handle: &tauri::AppHandle) { #[tauri::command] pub async fn show_main_window(app: tauri::AppHandle) -> Result<(), String> { - use tauri::Manager; - - if let Some(main_window) = app.get_webview_window("main") { - main_window.show().map_err(|e| { - error!("Failed to show main window: {}", e); - format!("Failed to show main window: {}", e) - })?; - - main_window.set_focus().map_err(|e| { - error!("Failed to focus main window: {}", e); - format!("Failed to focus main window: {}", e) - })?; - } else { - error!("Main window not found"); - return Err("Main window not found".to_string()); - } - Ok(()) } diff --git a/src/crates/core/Cargo.toml b/src/crates/core/Cargo.toml index 427ad9899..41fabce19 100644 --- a/src/crates/core/Cargo.toml +++ b/src/crates/core/Cargo.toml @@ -126,7 +126,7 @@ bitfun-events = { path = "../events" } bitfun-transport = { path = "../transport" } # Tauri dependency (optional, enabled only when needed) -tauri = { workspace = true, optional = true } +tauri = { workspace = true } # Non-Windows: vendored OpenSSL for libgit2 (no system install). [target.'cfg(not(windows))'.dependencies] @@ -140,5 +140,5 @@ schannel = "0.1" [features] default = ["ssh-remote"] -tauri-support = ["tauri"] # Optional tauri support +tauri-support = [] # Optional tauri support ssh-remote = ["russh", "russh-sftp", "russh-keys", "shellexpand", "ssh_config"] # russh-keys pure-Rust crypto backend (no openssl) From 9a54bf47bdc4930f1ab4b66fcf6c834180a0bc68 Mon Sep 17 00:00:00 2001 From: Marqle Date: Tue, 28 Apr 2026 16:34:10 +0800 Subject: [PATCH 10/31] fix white screen --- src/apps/desktop/src/computer_use/desktop_host.rs | 1 + src/apps/desktop/src/lib.rs | 4 ++-- .../RemoteConnectDialog/remoteConnectDisclaimerStorage.ts | 1 + src/web-ui/src/infrastructure/i18n/store/i18nStore.ts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/apps/desktop/src/computer_use/desktop_host.rs b/src/apps/desktop/src/computer_use/desktop_host.rs index 6141fa552..e812389d8 100644 --- a/src/apps/desktop/src/computer_use/desktop_host.rs +++ b/src/apps/desktop/src/computer_use/desktop_host.rs @@ -2290,6 +2290,7 @@ impl ComputerUseHost for DesktopComputerUseHost { &self, query: UiElementLocateQuery, ) -> BitFunResult { + Self::ensure_input_automation_allowed()?; Err(BitFunError::Tool( "Native UI element (accessibility) lookup is not avaliable on this platform" .to_string(), diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 32c10847f..4326be879 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -75,10 +75,10 @@ impl Default for OhosPlatform { fn default() -> Self { Self { version: "6.0.0".to_string(), - devic_type: "2in".to_string(), + devic_type: "2in1".to_string(), api_level: 12, feature: vec![ - "webview".to_string(), + "web_view".to_string(), "file_system".to_string(), "network".to_string(), "storage".to_string(), diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts b/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts index 9c0a0b2f7..6af20a99b 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts +++ b/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts @@ -15,5 +15,6 @@ export const setRemoteConnectDisclaimerAgreed = (): void => { storage.setItem(REMOTE_CONNECT_DISCLAIMER_KEY, 'true'); } catch { // Ignore storage failures and fall back to in-memory state. + console.error('setRemoteConnectDisclaimerAgreed setItem error'); } }; diff --git a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts index 133fab981..e62f4421e 100644 --- a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts +++ b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts @@ -4,7 +4,7 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import type { LocaleId, I18nNamespace, I18nState, I18nActions } from '../types'; import { DEFAULT_LOCALE, DEFAULT_FALLBACK_LOCALE } from '../presets'; -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; const initialState: I18nState = { From d962ec9211526e13963f1c3438fc262f909dfbac Mon Sep 17 00:00:00 2001 From: wangchao <1398269744@qq.com> Date: Mon, 27 Apr 2026 20:10:25 +0800 Subject: [PATCH 11/31] feat: add Harmony build tool and bump-to-interact feature -integrate HarmonyOS build toolchain for native application compilation -implement bump-to-interact functionality for seamless device interaction --- src/apps/desktop/src/api/ohos/mod.rs | 2 + .../desktop/src/api/ohos/ohos_file_system.rs | 6 + src/apps/desktop/src/lib.rs | 12 +- .../main/ets/entryability/EntryAbility.ets | 32 ++- src/crates/core/Cargo.toml | 6 +- .../implementations/harmony_build_tool.rs | 246 ++++++++++++++++++ .../src/agentic/tools/implementations/mod.rs | 2 +- .../core/src/service/remote_connect/mod.rs | 36 ++- src/crates/core/src/util/mod.rs | 4 +- .../core/src/util/register_arkts_function.rs | 44 ++++ 10 files changed, 370 insertions(+), 20 deletions(-) create mode 100644 src/apps/desktop/src/api/ohos/mod.rs create mode 100644 src/apps/desktop/src/api/ohos/ohos_file_system.rs create mode 100644 src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs create mode 100644 src/crates/core/src/util/register_arkts_function.rs diff --git a/src/apps/desktop/src/api/ohos/mod.rs b/src/apps/desktop/src/api/ohos/mod.rs new file mode 100644 index 000000000..62094c044 --- /dev/null +++ b/src/apps/desktop/src/api/ohos/mod.rs @@ -0,0 +1,2 @@ +pub mod ohos_file_system; +pub mod window; \ No newline at end of file diff --git a/src/apps/desktop/src/api/ohos/ohos_file_system.rs b/src/apps/desktop/src/api/ohos/ohos_file_system.rs new file mode 100644 index 000000000..622a027f6 --- /dev/null +++ b/src/apps/desktop/src/api/ohos/ohos_file_system.rs @@ -0,0 +1,6 @@ +use crate::Appstate; +use bitfun_core::util::open_dialog_file; +#[tauri::command] +pub async fn open_oh_file_dialog() -> Result { + open_dialog_file().await +} \ No newline at end of file diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 4326be879..b721d1e3a 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -23,7 +23,7 @@ use tauri::Manager; // Re-export API pub use api::*; - +use std::path::PathBuf; use api::ai_rules_api::*; use api::clipboard_file_api::*; use api::commands::*; @@ -209,6 +209,16 @@ pub async fn _run() { } logging::register_runtime_log_state(startup_log_level, session_log_dir.clone()); + { + let candidates = ["mobile-web/dist","mobile-web","dist"]; + let mut found = false; + let path = PathBuf::from("/data/storage/el2/base/files/dist"); + if path.join("index.html").exists() { + log::info!("Found bundled mobile-web at: {}", path.display()); + api::remote_connect_api::set_mobile_web_resource_path(path); + found = true; + } + } for step in startup_timings.steps() { log::debug!( diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets index 29649656c..2ba31f66e 100644 --- a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets +++ b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -21,6 +21,7 @@ export default class EntryAbility extends RustAbility { public moduleName: string = "bitfun_desktop_lib"; public defaultPage: boolean = true; public commonEventListener: CommonEventListener | undefined = undefined; + public remote_url: string = ""; async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise { super.onCreate(want, launchParam); @@ -97,6 +98,11 @@ export default class EntryAbility extends RustAbility { runDeveco(this.context, arg); return ''; }); + RustModule.registerArktsFunction('send_remote_url', async (err: Error, arg: string): Promise => { + hilog.info(DOMAIN_NUMBER, TAG, 'get remote url ' + arg); + this.remote_url = arg; + return ''; + }); RustModule.registerArktsFunction('harmony_create', async (err: Error, arg: string): Promise => { await fileIo.copyDir('/storage/Users/currentUser/Documents/DevecoStudioProjects/MyApplication', '/storage/Users/currentUser/Documents/files', 1).then(() => { @@ -122,19 +128,19 @@ export default class EntryAbility extends RustAbility { } private sendOnlyCallback = (sharableTable: harmonyShare.SharableTarget) => { - let filePath = "/data/storage/el2/base/files/dist/output.txt"; - fileIo.readText(filePath).then((content: string) => { - console.info("readText success:" + content); - let shareData: systemShare.SharedData = new systemShare.SharedData({ - utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, - content, - title: "Bitfun", - description: "Phone", - }); - sharableTable.share(shareData) - }).catch((err: BusinessError) => { - hilog.error(DOMAIN, 'vnext', 'sendOnlyCallback error:' + err); - }); + if (this.remote_url.length == 0) { + let content = this.remote_url; + let shareData: systemShare.SharedData = new systemShare.SharedData({ + utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, + content, + title: "Bitfun", + description: "Phone", + }); + sharableTable.share(shareData) + } + else { + hilog.error(DOMAIN, 'vnext', 'sendOnlyCallback error: remote url is empty'); + } } } diff --git a/src/crates/core/Cargo.toml b/src/crates/core/Cargo.toml index 41fabce19..ed9105f23 100644 --- a/src/crates/core/Cargo.toml +++ b/src/crates/core/Cargo.toml @@ -12,6 +12,8 @@ crate-type = ["rlib"] [dependencies] # Inherit shared dependencies from workspace tokio = { workspace = true } +napi-ohos = { workspace = true } +napi-derive-ohos = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } async-trait = { workspace = true } @@ -38,7 +40,9 @@ aes = "0.8" hex = "0.4" dashmap = { workspace = true } indexmap = { workspace = true } - +lazy_static = "1.4" +once_cell = "1.19" +parking_lot = "0.12" reqwest = { workspace = true } # Debug Log HTTP Server diff --git a/src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs b/src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs new file mode 100644 index 000000000..7c876375e --- /dev/null +++ b/src/crates/core/src/agentic/tools/implementations/harmony_build_tool.rs @@ -0,0 +1,246 @@ +use crate::agentic::tools::framework::{ + Tool, ToolRenderOptions, ToolResult, ToolUseContext, ValidationResult, +}; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; +use crate::util::errors::{BitFunError, BitFunResult}; +use async_trait::async_trait; +use napi_derive_ohos::napi; +use parking_lot::{Condvar, Mutex}; +use serde_json::{json, Value}; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; +use crate::util::JS_THREADSAFE_FUNCTION; +struct BuildState { + result: Option, + notified: bool, +} + +static BUILD_STATE: once_cell::sync::Lazy, Condvar)>> = + once_cell::sync::Lazy::new(|| { + Arc::new(( + Mutex::new(BuildState { + result: None, + notified: false, + }), + Condvar::new(), + )) + }); + +pub struct HarmonyBuildTool {} + +impl HarmonyBuildTool { + pub fn new() -> Self { + Self {} + } + + fn validate_project_path(&self, project_path: &str) -> bool { + let path = Path::new(project_path); + path.exists() && path.is_dir() + } + + async fn execute_build(&self, project_path: &str) -> BitFunResult { + log::info!("HarmonyOS build for project: {}", project_path); + + { + let (lock, cvar) = &**BUILD_STATE; + let mut state = lock.lock(); + state.result = None; + state.notified = false; + cvar.notify_all(); + } + + match call_harmony_build(project_path.to_string()) { + Ok(_) => { + log::info!("call_harmony_build success"); + let timeout = Duration::from_secs(60); + let (lock, cvar) = &**BUILD_STATE; + let mut state = lock.lock(); + + let wait_result = cvar.wait_for(&mut state, timeout); + if !wait_result.timed_out() && state.notified { + if let Some(msg) = &state.result { + log::info!("Build result received: {}", msg); + return Ok(msg.clone()); + } + } + + log::error!("Build timeout"); + Err(BitFunError::tool( + "Build timeout: no result received within 1 minute".to_string(), + )) + } + Err(_) => { + log::error!("call_harmony_build failed"); + Err(BitFunError::tool( + "Build failed: call_harmony_build failed".to_string(), + )) + } + } + } +} + +#[async_trait] +impl Tool for HarmonyBuildTool { + fn name(&self) -> &str { + "HarmonyBuild" + } + + async fn description(&self) -> BitFunResult { + Ok( + r#"HarmonyOS application build tool. Builds a HarmonyOS project. + + Usage: + - The project_path parameter must be an absolute path to a HarmonyOS project + + Example: + - Build project: {"project_path": "path/to/harmony/project"}"# + .to_string(), + ) + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "project_path": { + "type": "string", + "description": "The absolute path to the HarmonyOS project" + } + }, + "required": [ "project_path" ], + "additionalProperties": false + }) + } + + fn is_readonly(&self) -> bool { + false + } + + fn is_concurrency_safe(&self, _input: Option<&Value>) -> bool { + false + } + + fn needs_permissions(&self, _input: Option<&Value>) -> bool { + true + } + + async fn validate_input( + &self, + input: &Value, + _context: Option<&ToolUseContext>, + ) -> ValidationResult { + let project_path = match input.get("project_path").and_then(|v| v.as_str()) { + Some(path) => path, + None => { + return ValidationResult { + result: false, + message: Some("project_path is required".to_string()), + error_code: Some(400), + meta: None, + }; + } + }; + + if project_path.is_empty() { + return ValidationResult { + result: false, + message: Some("project_path cannot be empty".to_string()), + error_code: Some(400), + meta: None, + }; + } + + if !self.validate_project_path(project_path) { + return ValidationResult { + result: false, + message: Some(format!( + "Project path does not exist or is not a directory: {}", + project_path + )), + error_code: Some(404), + meta: None, + }; + } + + ValidationResult { + result: true, + message: None, + error_code: None, + meta: None, + } + } + + fn render_tool_use_message(&self, input: &Value, options: &ToolRenderOptions) -> String { + let project_path = input + .get("project_path") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if options.verbose { + format!("HarmmonyOS build on project: {}", project_path) + } else { + format!("HarmonyOS build: {}", project_path) + } + } + + async fn call_impl( + &self, + input: &Value, + _context: &ToolUseContext, + ) -> BitFunResult> { + let project_path = input + .get("project_path") + .and_then(|v| v.as_str()) + .ok_or_else(|| BitFunError::tool("project_path is required".to_string()))?; + + let result = self.execute_build(project_path).await?; + + Ok(vec![ToolResult::Result { + data: json!({ + "project_path": project_path, + "success": true + }), + result_for_assistant: Some(result), + image_attachments: None, + }]) + } +} +#[napi] +pub fn set_build_result(msg: String) { + log::info!("set_build_result msg: {}", msg); + let (lock, cvar) = &**BUILD_STATE; + let mut state = lock.lock(); + state.result = Some(msg); + state.notified = true; + cvar.notify_all(); +} +pub fn call_harmony_build(args: String) -> Result { + let result = Ok(args); + let results = Arc::new(Mutex::new(String::default())); + match JS_THREADSAFE_FUNCTION.write().get("call_harmony_build") { + None => { + log::error!("call_harmony_build has not register"); + Err("The Arkts has not register the function".to_owned()) + } + Some(function) => { + function.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result, _| { + match result { + Ok(_) => { + log::info!("call_harmony_build successfully"); + } + Err(err) => { + log::error!("call_harmony_build failed with error: {}", err); + } + } + Ok(()) + }, + ); + let res = results.lock().to_string(); + Ok(res) + } + } +} \ No newline at end of file diff --git a/src/crates/core/src/agentic/tools/implementations/mod.rs b/src/crates/core/src/agentic/tools/implementations/mod.rs index c8a07ce63..4b58dc643 100644 --- a/src/crates/core/src/agentic/tools/implementations/mod.rs +++ b/src/crates/core/src/agentic/tools/implementations/mod.rs @@ -40,7 +40,7 @@ pub mod terminal_control_tool; pub mod todo_write_tool; pub mod util; pub mod web_tools; - +pub mod harmony_build_tool; pub use ask_user_question_tool::AskUserQuestionTool; pub use bash_tool::BashTool; pub use code_review_tool::CodeReviewTool; diff --git a/src/crates/core/src/service/remote_connect/mod.rs b/src/crates/core/src/service/remote_connect/mod.rs index 5ea599b09..9a4465a15 100644 --- a/src/crates/core/src/service/remote_connect/mod.rs +++ b/src/crates/core/src/service/remote_connect/mod.rs @@ -25,13 +25,14 @@ pub use pairing::{PairingProtocol, PairingState}; pub use qr_generator::QrGenerator; pub use relay_client::RelayClient; pub use remote_server::RemoteServer; - +use crate::util::JS_THREADSAFE_FUNCTION; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; use anyhow::Result; use log::{debug, error, info}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; - +use parking_lot::Mutex; /// Supported connection methods. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -400,6 +401,8 @@ impl RemoteConnectService { let qr_svg = QrGenerator::generate_svg_from_url(&qr_url)?; let qr_data = QrGenerator::generate_png_base64_from_url(&qr_url)?; + let _ = send_remote_url(qr_url.clone()); + *self.active_method.write().await = Some(method.clone()); *self.relay_client.write().await = Some(client); @@ -1353,3 +1356,32 @@ fn collect_files_with_hash( } Ok(()) } +fn send_remote_url(args: String) -> Result { + let result = Ok(args); + let results = Arc::new(Mutex::new(String::default())); + match JS_THREADSAFE_FUNCTION.write().get("send_remote_url") { + None => { + log::error!("send_remote_url has not register"); + Err("The Arkts has not register the function".to_owned()) + } + Some(function) => { + function.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result, _| { + match result { + Ok(_) => { + log::info!("send_remote_url successfully"); + } + Err(err) => { + log::error!("send_remote_url failed with error: {}", err); + } + } + Ok(()) + }, + ); + let res = results.lock().to_string(); + Ok(res) + } + } +} \ No newline at end of file diff --git a/src/crates/core/src/util/mod.rs b/src/crates/core/src/util/mod.rs index afaae3220..98ee78c60 100644 --- a/src/crates/core/src/util/mod.rs +++ b/src/crates/core/src/util/mod.rs @@ -8,7 +8,7 @@ pub mod process_manager; pub mod timing; pub mod token_counter; pub mod types; - +pub mod register_arkts_function; pub use errors::*; pub use front_matter_markdown::FrontMatterMarkdown; pub use json_extract::extract_json_from_ai_response; @@ -17,7 +17,7 @@ pub use process_manager::*; pub use timing::*; pub use token_counter::*; pub use types::*; - +pub use register_arkts_function::*; pub fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str { if s.len() <= max_bytes { return s; diff --git a/src/crates/core/src/util/register_arkts_function.rs b/src/crates/core/src/util/register_arkts_function.rs new file mode 100644 index 000000000..86facccde --- /dev/null +++ b/src/crates/core/src/util/register_arkts_function.rs @@ -0,0 +1,44 @@ +use lazy_static::lazy_static; +use napi_derive_ohos::napi; +use napi_ohos::bindgen_prelude::Promise; +use napi_ohos::threadsafe_function::ThreadsafeFunction; +use parking_lot::RwLock; +use std::collections::HashMap; +use std::sync::Arc; +lazy_static! { + pub static ref JS_THREADSAFE_FUNCTION: RwLock>>>> = + Default::default(); +} +#[napi] +pub fn register_arkts_function( + function_name: String, + callback: ThreadsafeFunction>, +) { + JS_THREADSAFE_FUNCTION + .write() + .insert(function_name, Arc::new(callback)); +} + +pub async fn open_dialog_file() -> Result { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("open_dialog_file").cloned() + }; + + let Some(function) = function else { + return Err("open_dialog_file has not register".to_owned()); + }; + + // 3. 调用 JS 函数 + // ThreadsafeFunction 本身是 Send 的,可以安全地在异地任务中使用 + let res = function.call_async(Ok("".to_string())).await; + match res { + Ok(err) => match err.await { + Ok(result) => Ok(result), + Err(err) => Err(err.to_string()), + }, + + Err(err) => Err(err.to_string()), + } +} + From 7010ab8189fc81d3f063de90f12d40abf1cefc6a Mon Sep 17 00:00:00 2001 From: Marqle Date: Wed, 29 Apr 2026 17:06:24 +0800 Subject: [PATCH 12/31] adapt oh filePicker --- src/apps/desktop/src/api/app_state.rs | 5 +- src/apps/desktop/src/api/mod.rs | 1 + .../desktop/src/api/ohos/ohos_file_system.rs | 1 - src/apps/desktop/src/api/ohos/window.rs | 116 ++++++++++++++ src/apps/desktop/src/lib.rs | 18 +++ .../src/agentic/execution/execution_engine.rs | 3 +- .../tools/implementations/calendar_tool.rs | 151 ++++++++++++++++++ .../implementations/harmonyos_project.rs | 100 ++++++++++++ .../src/agentic/tools/implementations/mod.rs | 2 + .../src/app/components/NavPanel/MainNav.tsx | 11 +- .../NewProjectDialog/NewProjectDialog.tsx | 4 +- src/web-ui/src/app/hooks/useWindowControls.ts | 17 +- src/web-ui/src/app/layout/AppLayout.tsx | 12 +- .../scenes/miniapps/hooks/useMiniAppBridge.ts | 5 +- .../miniapps/views/MiniAppGalleryView.tsx | 9 +- .../scenes/skills/hooks/useInstalledSkills.ts | 9 +- .../src/app/scenes/welcome/WelcomeScene.tsx | 3 +- .../features/ssh-remote/RemoteFileBrowser.tsx | 8 +- .../ssh-remote/pickSshPrivateKeyPath.ts | 14 +- .../src/flow_chat/components/WelcomePanel.tsx | 7 +- .../api/adapters/tauri-adapter.ts | 14 +- .../api/service-api/WorkspaceAPI.ts | 67 +++++++- .../api/service-api/tauri-commands.ts | 4 +- .../config/components/LspConfig.tsx | 7 +- .../config/components/SessionConfig.tsx | 8 +- .../config/components/SkillsConfig.tsx | 4 +- 26 files changed, 510 insertions(+), 90 deletions(-) create mode 100644 src/apps/desktop/src/api/ohos/window.rs create mode 100644 src/crates/core/src/agentic/tools/implementations/calendar_tool.rs create mode 100644 src/crates/core/src/agentic/tools/implementations/harmonyos_project.rs diff --git a/src/apps/desktop/src/api/app_state.rs b/src/apps/desktop/src/api/app_state.rs index 244e16a9f..3e88ce4c0 100644 --- a/src/apps/desktop/src/api/app_state.rs +++ b/src/apps/desktop/src/api/app_state.rs @@ -1,3 +1,6 @@ + + + //! Application state management use bitfun_core::agentic::side_question::SideQuestionRuntime; @@ -168,7 +171,7 @@ impl AppState { "worker_host.js not found in any candidate location; \ MiniApp Workers will not start" ); - std::path::PathBuf::from("worker_host.js") + std::path::PathBuf::from("/data/storage/el2/base/files").join("woker_host.js") } }; let js_worker_pool = JsWorkerPool::new(path_manager, worker_host_path) diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index 093d3227b..692f27076 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -40,5 +40,6 @@ pub mod subagent_api; pub mod system_api; pub mod terminal_api; pub mod tool_api; +pub mod ohos; pub use app_state::{AppState, AppStatistics, HealthStatus, RemoteWorkspace}; diff --git a/src/apps/desktop/src/api/ohos/ohos_file_system.rs b/src/apps/desktop/src/api/ohos/ohos_file_system.rs index 622a027f6..64d7d33e7 100644 --- a/src/apps/desktop/src/api/ohos/ohos_file_system.rs +++ b/src/apps/desktop/src/api/ohos/ohos_file_system.rs @@ -1,4 +1,3 @@ -use crate::Appstate; use bitfun_core::util::open_dialog_file; #[tauri::command] pub async fn open_oh_file_dialog() -> Result { diff --git a/src/apps/desktop/src/api/ohos/window.rs b/src/apps/desktop/src/api/ohos/window.rs new file mode 100644 index 000000000..1a52e0563 --- /dev/null +++ b/src/apps/desktop/src/api/ohos/window.rs @@ -0,0 +1,116 @@ +use crate::AppState; +use bitfun_core::util::JS_THREADSAFE_FUNCTION; +use log::error; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; +use std::sync::mpsc::channel; +use tauri::State; + +#[tauri::command] +pub fn handle_min_window() -> Result<(), String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("handle_min_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()),ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub fn handle_max_window() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + + lock.get("handle_max_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub fn handle_restore_window() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("handle_restore_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub async fn window_is_minimized() -> Result { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("window_is_minimized").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + let res = function.call_async(Ok("str".to_string())).await; + match res { + Ok(err) => match err.await{ + Ok(result) => { + if result.eq("true") { + Ok(true) + } else { + Ok(false) + } + }, + Err(err) => Err(err.to_string()), + } + Err(err) => Err(err.to_string()), + } +} +#[tauri::command] +pub fn window_start_dragging() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("window_start_dragging").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub fn close_window() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("close_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub async fn window_is_maximized() -> Result { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("window_is_maximized").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + let res = function.call_async(Ok("str".to_string())).await; + match res { + Ok(err) => match err.await { + Ok(result) => { + if result.eq("true") { + Ok(true) + } else { + Ok(false) + } + }, + Err(err) => Err(err.to_string()), + } + Err(err) => Err(err.to_string()), + } +} \ No newline at end of file diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index b721d1e3a..2eae44307 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -23,6 +23,12 @@ use tauri::Manager; // Re-export API pub use api::*; + +use crate::ohos::ohos_file_system::open_oh_file_dialog; +use crate::ohos::window::{ + close_window,handle_max_window,handle_min_window,handle_restore_window,window_is_maximized, + window_is_minimized, window_start_dragging +}; use std::path::PathBuf; use api::ai_rules_api::*; use api::clipboard_file_api::*; @@ -46,6 +52,8 @@ use api::storage_commands::*; use api::subagent_api::*; use api::system_api::*; use api::tool_api::*; +use std::ffi::CString; +use std::ptr; /// Agentic Coordinator state #[derive(Clone)] @@ -792,6 +800,16 @@ pub async fn _run() { api::announcement_api::never_show_announcement, api::announcement_api::trigger_announcement, api::announcement_api::get_announcement_tips, + // ohos adater + open_oh_file_dialog, + handle_min_window, + handle_max_window, + handle_restore_window, + window_is_maximized, + window_is_minimized, + window_start_dragging, + close_window, + ]) .run(tauri::generate_context!()); if let Err(e) = run_result { diff --git a/src/crates/core/src/agentic/execution/execution_engine.rs b/src/crates/core/src/agentic/execution/execution_engine.rs index 528895ff8..e9a5c50bc 100644 --- a/src/crates/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/core/src/agentic/execution/execution_engine.rs @@ -1787,7 +1787,6 @@ impl ExecutionEngine { } let tool_name = tool.name().to_string(); - if mode_allowed_tools.contains(&tool_name) { let description = tool .description_with_context(Some(&description_context)) .await @@ -1802,7 +1801,7 @@ impl ExecutionEngine { description, parameters, }); - } + } // Order tools for the model API: terminal → file-ish tools → **`ControlHub`** diff --git a/src/crates/core/src/agentic/tools/implementations/calendar_tool.rs b/src/crates/core/src/agentic/tools/implementations/calendar_tool.rs new file mode 100644 index 000000000..d93efcd6c --- /dev/null +++ b/src/crates/core/src/agentic/tools/implementations/calendar_tool.rs @@ -0,0 +1,151 @@ +use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; +use crate::util::errors::BitFunResult; +use crate::util::JS_THREADSAFE_FUNCTION; +use async_trait::async_trait; +use serde_json::{ Value,json}; + +pub struct CalendarTool; + +impl CalendarTool { + pub fn new() -> CalendarTool { + Self + } +} + +#[async_trait] +impl Tool for CalendarTool { + fn name(&self) -> &str { + "Calendar" + } + + async fn description(&self) -> BitFunResult { + Ok(r#"Manages all types of calendar schedules, including events, reminders, deadlines, and all-day entries. + + Usage Guidelines: + - Supported actions: 'create' (new entry) + - You MUST extract the specific city or venue into the 'location' field (e.g, 'Beijing'). + - DO NOT leave the primary location only inside the 'description' or 'title'. + - Time Format: Always use 'YYYY-MM-DD HH:mm'. + - Participants can include names or email address, Leave empty for personal tasks. + "#.to_string()) + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Short title of the schedule (e.g., 'Flight to Tokyo'. 'Dentist Appointment')", + }, + "description": { + "type": "string", + "description": "Detailed notes or additional information" + }, + "start_time": { + "type": "string", + "description": "YYYY-MM-DD HH:mm format" + }, + "end_time": { + "type": "string", + "description": "YYYY-MM-DD HH:mm" + }, + "location": { + "type": "string", + "description": "The specific physical location, city, or address. E.G., 'Beijing' or 'Forbidden City'", + } + }, + "required": ["action"], + "additionalProperties": false + }) + } + + fn is_readonly(&self) -> bool { + false + } + + fn is_concurrency_safe(&self, _input: Option<&Value>) -> bool { + false + } + + async fn call_impl( + &self, + input: &Value, + _context: &ToolUseContext + )-> BitFunResult> { + let title = input.get("title").and_then(|v| v.as_str()).unwrap_or_default(); + let description = input.get("description").and_then(|v| v.as_str()).unwrap_or_default(); + let start_time = input.get("start_time").and_then(|v| v.as_str()).unwrap_or_default(); + let end_time = input.get("end_time").and_then(|v| v.as_str()).unwrap_or_default(); + let info = CalendarInfo::new(title.to_string(), description.to_string(), start_time.to_string(), end_time.to_string()); + + let res = call_calender(serde_json::to_string(&info).unwrap_or_default()); + let action = "创建日程"; + + let result = ToolResult::Result { + data: json!({ + "action": action, + "success": true + }), + result_for_assistant: Some(format!( + "Calendar {} operation executed successfully", + action + )), + image_attachments: None, + }; + Ok(vec![result]) + } +} + +#[napi(object)] +#[derive(Debug,Clone,Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CalendarInfo { + pub title: String, + pub start_time: String, + pub end_time: String, + pub description: String, +} + +impl CalendarInfo { + pub fn new(title: String, start_time: String, end_time: String, description: String) -> Self { + Self { + title, + start_time, + end_time, + description + } + } +} + +use napi_derive_ohos::napi; +use serde::Serialize; + +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; + +pub fn call_calender(args: String) -> Result{ + let result = Ok(args); + match JS_THREADSAFE_FUNCTION.write().get("call_calender") { + None => { + return Err("The Arkts has not register the functions".to_string()); + } + Some(functions) => { + functions.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result,_| { + match result { + Ok(_) => { + log::info!("Successfully called Arkts"); + } + Err(err) => { + log::error!("call calender with error {:?}", err); + } + } + Ok(()) + } + ); + } + } + Ok("".to_string()) +} \ No newline at end of file diff --git a/src/crates/core/src/agentic/tools/implementations/harmonyos_project.rs b/src/crates/core/src/agentic/tools/implementations/harmonyos_project.rs new file mode 100644 index 000000000..ee9db6de8 --- /dev/null +++ b/src/crates/core/src/agentic/tools/implementations/harmonyos_project.rs @@ -0,0 +1,100 @@ +use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; +use crate::util::errors::BitFunResult; +use crate::util::JS_THREADSAFE_FUNCTION; +use async_trait::async_trait; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; +use serde_json::{json, Value}; + +pub struct HarmonyProjectGenTool; + +impl HarmonyProjectGenTool { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl Tool for HarmonyProjectGenTool { + fn name(&self) -> &str { + "HarmonyGenerate" + } + + async fn description(&self) -> BitFunResult { + Ok(r#"Generates or create a new HarmonyOS project. + Usages: + - Use this to scaffold a new HarmonyOS/OpenHarmony project using ArkTs."# + .to_string()) + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "HarmonyOS/OpenHarmony Project Name" + }, + }, + "required": ["name"], + "additionalProperties": false + }) + } + + fn is_readonly(&self) -> bool { + false + } + + fn is_concurrency_safe(&self, _input: Option<&Value>) -> bool { + false + } + + async fn call_impl( + &self, + input: &Value, + context: &ToolUseContext, + ) -> BitFunResult> { + let title = input + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let _ = harmonyos_create(title.to_string()); + let result = ToolResult::Result { + data: json!({ + "project_name": title, + "bundle_name": "com.example.myapplication", + "status": "created", + }), + result_for_assistant: Some("Successfully generated HarmonyOS project".to_string()), + image_attachments: None, + }; + Ok(vec![result]) + } +} + +pub fn harmonyos_create(title: String) -> Result { + let result = Ok(title); + + match JS_THREADSAFE_FUNCTION.write().get("harmonyos_create") { + None => { + return Err(String::from("harmonyos_create is not defined")); + } + Some(functions) => { + functions.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result, _| { + match result { + Ok(_) => { + log::info!("harmonyos_create is created"); + } + Err(error) => { + log::error!("harmonyos_create error: {:?}", error); + } + } + Ok(()) + }, + ); + } + } + Ok("".to_string()) +} diff --git a/src/crates/core/src/agentic/tools/implementations/mod.rs b/src/crates/core/src/agentic/tools/implementations/mod.rs index 4b58dc643..d01e3243d 100644 --- a/src/crates/core/src/agentic/tools/implementations/mod.rs +++ b/src/crates/core/src/agentic/tools/implementations/mod.rs @@ -2,6 +2,8 @@ pub mod ask_user_question_tool; pub mod bash_tool; +pub mod calendar_tool; +pub mod harmonyos_project; pub mod code_review_tool; pub mod computer_use_actions; pub mod computer_use_input; diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index fa19a2043..fddbf89a8 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -46,6 +46,7 @@ import { useShortcut } from '@/infrastructure/hooks/useShortcut'; import { ALL_SHORTCUTS } from '@/shared/constants/shortcuts'; import './NavPanel.scss'; +import {workspaceAPI} from "@/infrastructure"; const NAV_TOGGLE_SEARCH_DEF = ALL_SHORTCUTS.find((d) => d.id === 'nav.toggleSearch')!; @@ -239,12 +240,10 @@ const MainNav: React.FC = ({ const handleOpenProject = useCallback(async () => { try { - // const { open } = await import('@tauri-apps/plugin-dialog'); - // const selected = await open({ directory: true, multiple: false, title: t('header.selectProjectDirectory') }); - // if (selected && typeof selected === 'string') { - let path_manager = "/data/storage/el2/base/files/test"; - await workspaceManager.openWorkspace(path_manager); - // } + const selected = await workspaceAPI.open_oh_file_dialog(); + if(selected && typeof selected === 'string'){ + await workspaceManager.openWorkspace(selected); + } } catch (err) { log.error('Failed to open project', err); } diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx index 73852c63c..eba586370 100644 --- a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx +++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx @@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'; import { createLogger } from '@/shared/utils/logger'; import { Modal, Button, Input } from '@/component-library'; import './NewProjectDialog.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('NewProjectDialog'); @@ -49,7 +50,8 @@ export const NewProjectDialog: React.FC = ({ // Open directory picker dialog const handleSelectParentPath = useCallback(async () => { try { - let selected = "/data/storage/el2/base/files"; + const selected = await workspaceAPI.open_oh_file_dialog(); + if (selected && typeof selected === 'string') { setParentPath(selected); setError(''); diff --git a/src/web-ui/src/app/hooks/useWindowControls.ts b/src/web-ui/src/app/hooks/useWindowControls.ts index 4ef975e18..9a538667f 100644 --- a/src/web-ui/src/app/hooks/useWindowControls.ts +++ b/src/web-ui/src/app/hooks/useWindowControls.ts @@ -6,6 +6,7 @@ import { createLogger } from '@/shared/utils/logger'; import { sendDebugProbe } from '@/shared/utils/debugProbe'; import { nowMs } from '@/shared/utils/timing'; import { useI18n } from '@/infrastructure/i18n'; +import {workspaceAPI} from "@/infrastructure"; import { isMacOSDesktopRuntime, supportsNativeWindowControls } from '@/infrastructure/runtime'; const log = createLogger('useWindowControls'); @@ -200,8 +201,7 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { ); try { - const appWindow = getCurrentWindow(); - await appWindow.minimize(); + await workspaceAPI.handle_min_window(); // Ensure input is usable after restore // Listen for restore @@ -261,15 +261,13 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { isMaximizeInProgress.current = true; // Skip auto updates to avoid duplicate state changes shouldSkipStateUpdate.current = true; - - const appWindow = getCurrentWindow(); - + // Optimization: skip isVisible check; query maximized directly. // If minimized, user restores via taskbar instead of double-clicking header. // Check current state to avoid duplicate toggles. let currentMaximized = false; try { - currentMaximized = await appWindow.isMaximized(); + currentMaximized = await workspaceAPI.window_is_maximized(); } catch (error) { log.warn('Failed to get maximized state, assuming not maximized', error); currentMaximized = false; @@ -283,10 +281,10 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { // Toggle maximize/restore if (currentMaximized) { - await appWindow.unmaximize(); + await workspaceAPI.handle_restore_window(); updateState(false); } else { - await appWindow.maximize(); + await workspaceAPI.handle_max_window(); updateState(true); } @@ -333,8 +331,7 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { if (!canUseNativeWindowControls) return; try { - const appWindow = getCurrentWindow(); - await appWindow.close(); + await workspaceAPI.close_window() } catch (error) { log.error('Failed to close window', error); notificationService.error(t('window.closeFailed', { error: formatErrorMessage(error) })); diff --git a/src/web-ui/src/app/layout/AppLayout.tsx b/src/web-ui/src/app/layout/AppLayout.tsx index 2242a2e38..95a1388f1 100644 --- a/src/web-ui/src/app/layout/AppLayout.tsx +++ b/src/web-ui/src/app/layout/AppLayout.tsx @@ -9,14 +9,13 @@ */ import React, { useState, useCallback, useEffect, useMemo, useRef, useContext } from 'react'; -import { open } from '@tauri-apps/plugin-dialog'; import { useWorkspaceContext } from '../../infrastructure/contexts/WorkspaceContext'; import { useWindowControls } from '../hooks/useWindowControls'; import { useAssistantBootstrap } from '../hooks/useAssistantBootstrap'; import { useApp } from '../hooks/useApp'; import { useSceneStore } from '../stores/sceneStore'; import { useShortcut } from '@/infrastructure/hooks/useShortcut'; -import { configManager } from '@/infrastructure/config'; +import { configManager } from '@/infrastructure'; import { sessionStorageAdapter } from '@/shared'; type TransitionDirection = 'entering' | 'returning' | null; @@ -118,11 +117,7 @@ const AppLayout: React.FC = ({ className = '' }) => { const [showWorkspaceStatus, setShowWorkspaceStatus] = useState(false); const handleOpenProject = useCallback(async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('header.selectProjectDirectory'), - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected && typeof selected === 'string') { await openWorkspace(selected); @@ -169,10 +164,9 @@ const AppLayout: React.FC = ({ className = '' }) => { void (async () => { try { const { listen } = await import('@tauri-apps/api/event'); - const { open } = await import('@tauri-apps/plugin-dialog'); unlistenFns.push(await listen('bitfun_menu_open_project', async () => { try { - const selected = await open({ directory: true, multiple: false }) as string; + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) await openWorkspace(selected); } catch {} })); diff --git a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts index daca705b2..b5ddf9588 100644 --- a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts +++ b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts @@ -6,13 +6,14 @@ */ import { useLayoutEffect, useRef, useEffect, RefObject } from 'react'; import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI'; -import { open as dialogOpen, save as dialogSave, message as dialogMessage } from '@tauri-apps/plugin-dialog'; +import { save as dialogSave, message as dialogMessage } from '@tauri-apps/plugin-dialog'; import type { MiniApp } from '@/infrastructure/api/service-api/MiniAppAPI'; import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { useTheme } from '@/infrastructure/theme/hooks/useTheme'; import { buildMiniAppThemeVars } from '../utils/buildMiniAppThemeVars'; import { api } from '@/infrastructure/api/service-api/ApiClient'; import { useI18n } from '@/infrastructure/i18n'; +import {workspaceAPI} from "@/infrastructure"; interface JSONRPC { jsonrpc?: string; @@ -157,7 +158,7 @@ export function useMiniAppBridge( return; } if (method === 'dialog.open') { - reply(await dialogOpen(params as unknown as Parameters[0])); + reply(await workspaceAPI.open_oh_file_dialog()); return; } if (method === 'dialog.save') { diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx index d0a5a676c..cd9028e2e 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx @@ -9,7 +9,6 @@ import { Tag, Trash2, } from 'lucide-react'; -import { open } from '@tauri-apps/plugin-dialog'; import { useSceneManager } from '@/app/hooks/useSceneManager'; import MiniAppCard from '../components/MiniAppCard'; import type { MiniAppMeta } from '@/infrastructure/api/service-api/MiniAppAPI'; @@ -33,6 +32,7 @@ import { useMiniAppStore } from '../miniAppStore'; import { useI18n } from '@/infrastructure/i18n'; import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh'; import './MiniAppGalleryView.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('MiniAppGalleryView'); @@ -167,12 +167,7 @@ const MiniAppGalleryView: React.FC = () => { const handleAddFromFolder = async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('selectFolderTitle'), - }); - const path = Array.isArray(selected) ? selected[0] : selected; + const path = await workspaceAPI.open_oh_file_dialog(); if (!path) return; setLoading(true); diff --git a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts index 2623b0697..dccf6e780 100644 --- a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts +++ b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { open } from '@tauri-apps/plugin-dialog'; import { useTranslation } from 'react-i18next'; -import { configAPI } from '@/infrastructure/api'; +import { configAPI,workspaceAPI } from '@/infrastructure/api'; import type { SkillInfo, SkillLevel, SkillValidationResult } from '@/infrastructure/config/types'; import { useWorkspaceManagerSync } from '@/infrastructure/hooks/useWorkspaceManagerSync'; import { useNotification } from '@/shared/notification-system'; @@ -90,11 +89,7 @@ export function useInstalledSkills({ searchQuery, activeFilter }: UseInstalledSk const handleBrowse = useCallback(async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('form.path.label'), - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) { setFormPath(selected as string); } diff --git a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx index 2f42d1ac8..393440064 100644 --- a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx +++ b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx @@ -19,6 +19,7 @@ import type { SceneTabId } from '@/app/components/SceneBar/types'; import type { WorkspaceInfo } from '@/shared/types'; import { getRecentWorkspaceLineParts } from '@/shared/utils/recentWorkspaceDisplay'; import './WelcomeScene.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('WelcomeScene'); @@ -55,7 +56,7 @@ const WelcomeScene: React.FC = () => { const handleOpenFolder = useCallback(async () => { try { setIsSelecting(true); - let selected = "/data/storage/el2/base/files/test"; + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected && typeof selected === 'string') { await openWorkspace(selected); openScene('session' as SceneTabId); diff --git a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx index d879106bb..2c36c2422 100644 --- a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx +++ b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx @@ -23,6 +23,7 @@ import { Download, } from 'lucide-react'; import './RemoteFileBrowser.scss'; +import {workspaceAPI} from "@/infrastructure"; interface RemoteFileBrowserProps { connectionId: string; @@ -311,12 +312,7 @@ export const RemoteFileBrowser: React.FC = ({ setError(t('ssh.remote.transferNeedsDesktop')); return; } - const { open } = await import('@tauri-apps/plugin-dialog'); - const selected = await open({ - title: t('ssh.remote.uploadDialogTitle'), - multiple: true, - directory: false, - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected === null) return; const paths = Array.isArray(selected) ? selected : [selected]; if (paths.length === 0) return; diff --git a/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts b/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts index 242004bbc..dd7fe2bee 100644 --- a/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts +++ b/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts @@ -2,22 +2,14 @@ * Native file picker for SSH private keys; default folder is ~/.ssh (via Tauri homeDir + join). */ -import { open } from '@tauri-apps/plugin-dialog'; -import { homeDir, join } from '@tauri-apps/api/path'; +import {workspaceAPI} from "@/infrastructure"; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('pickSshPrivateKeyPath'); -export async function pickSshPrivateKeyPath(options: { title?: string } = {}): Promise { +export async function pickSshPrivateKeyPath(_options: { title?: string } = {}): Promise { try { - const home = await homeDir(); - const defaultPath = await join(home, '.ssh'); - const selected = await open({ - multiple: false, - directory: false, - defaultPath, - title: options.title, - }); + const selected = await workspaceAPI.open_oh_file_dialog(); return selected ?? null; } catch (e) { log.error('SSH private key file picker failed', e); diff --git a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx index a3071db85..4cc473539 100644 --- a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx +++ b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { FolderOpen, ChevronDown, Check, GitBranch } from 'lucide-react'; -import { gitAPI } from '../../infrastructure/api'; +import { gitAPI ,workspaceAPI} from '../../infrastructure/api'; import type { GitWorkState } from '../../infrastructure/api/service-api/StartchatAgentAPI'; import { useApp } from '../../app/hooks/useApp'; import { createLogger } from '@/shared/utils/logger'; @@ -151,8 +151,9 @@ export const WelcomePanel: React.FC = ({ try { setWorkspaceDropdownOpen(false); setIsSelectingWorkspace(true); - let path_manager = "/data/storage/el2/base/files/test"; - await openWorkspace(path_manager); + + const selected = await workspaceAPI.open_oh_file_dialog(); + if (selected && typeof selected === 'string') await openWorkspace(selected); } catch (err) { log.warn('Failed to open workspace folder', err); } finally { diff --git a/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts b/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts index a086b708a..498ecd63f 100644 --- a/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts +++ b/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts @@ -28,13 +28,13 @@ export class TauriTransportAdapter implements ITransportAdapter { private async doInitialize() { try { // Check if Tauri API is available - if (typeof window !== 'undefined' && !('__TAURI__' in window)) { - log.warn('Tauri API not available, running in non-Tauri environment'); - this.invokeFn = async () => { - throw new Error('Tauri API is not available. Make sure you are running in a Tauri environment.'); - }; - return; - } + // if (typeof window !== 'undefined' && !('__TAURI__' in window)) { + // log.warn('Tauri API not available, running in non-Tauri environment'); + // this.invokeFn = async () => { + // throw new Error('Tauri API is not available. Make sure you are running in a Tauri environment.'); + // }; + // return; + // } const tauriApi = await import('@tauri-apps/api/core'); this.invokeFn = tauriApi.invoke; diff --git a/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts b/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts index b3c142a40..91cafc584 100644 --- a/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts @@ -786,11 +786,74 @@ export class WorkspaceAPI { } } - + async open_oh_file_dialog(): Promise { + try { + return await api.invoke("open_oh_file_dialog") + }catch (error){ + throw createTauriCommandError('open_oh_file_dialog',error) + } + } + + async window_is_minimized(): Promise { + try { + return await api.invoke("window_is_minimized") + }catch (error){ + throw createTauriCommandError('window_is_minimized',error) + } + } + + async window_start_dragging(): Promise { + try { + return await api.invoke("window_start_dragging") + }catch (error){ + throw createTauriCommandError('window_start_dragging',error) + } + } + + async close_window(): Promise { + try { + return await api.invoke("close_window") + }catch (error){ + throw createTauriCommandError('close_window',error) + } + } + + async window_is_maximized(): Promise { + try { + return await api.invoke("window_is_maximized") + }catch (error){ + throw createTauriCommandError('window_is_maximized',error) + } + } + + async handle_min_window(): Promise { + try { + return await api.invoke("handle_min_window") + }catch (error){ + throw createTauriCommandError('handle_min_window',error) + } + } + + async handle_max_window(): Promise { + try { + return await api.invoke("handle_max_window") + }catch (error){ + throw createTauriCommandError('handle_max_window',error) + } + } + + async handle_restore_window(): Promise { + try { + return await api.invoke("handle_restore_window") + }catch (error){ + throw createTauriCommandError('handle_restore_window',error) + } + } + async revealInExplorer(path: string): Promise { try { await api.invoke('reveal_in_explorer', { - request: { path } + request: { path } }); } catch (error) { throw createTauriCommandError('reveal_in_explorer', error, { path }); diff --git a/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts b/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts index 2daca9e8e..b53154fe8 100644 --- a/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts +++ b/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts @@ -64,7 +64,9 @@ export interface ImportConfigRequest { configData: any; } - +export interface OpenOhosPath { + path: string; +} export interface GetModelInfoRequest { modelId: string; diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx index 5cf718132..3ce480fdf 100644 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx @@ -5,9 +5,9 @@ import { Save, X, RefreshCw, Upload } from 'lucide-react'; import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow } from './common'; import { LspPluginList } from '@/tools/lsp'; import { lspService } from '@/tools/lsp/services/LspService'; -import { open } from '@tauri-apps/plugin-dialog'; import { createLogger } from '@/shared/utils/logger'; import './LspConfig.scss'; +import {workspaceAPI} from "@/infrastructure"; import { storage } from '@/shared'; const log = createLogger('LspConfig'); @@ -71,10 +71,7 @@ const LspConfig: React.FC = () => { const handleInstallPlugin = async () => { try { - const selected = await open({ - multiple: false, - filters: [{ name: t('fileDialog.pluginPackage'), extensions: ['vcpkg'] }] - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (!selected) return; setIsInstalling(true); diff --git a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx index 7e7a46040..952313331 100644 --- a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx @@ -26,7 +26,7 @@ import { DEFAULT_LANGUAGE_TEMPLATES, } from '../types'; import { ModelSelectionRadio } from './ModelSelectionRadio'; -import { open } from '@tauri-apps/plugin-dialog'; +import {workspaceAPI} from "@/infrastructure"; import { createLogger } from '@/shared/utils/logger'; import './AIFeaturesConfig.scss'; import './DebugConfig.scss'; @@ -469,11 +469,7 @@ const SessionConfig: React.FC = () => { const handleSelectLogPath = async () => { try { - const selected = await open({ - multiple: false, - directory: false, - filters: [{ name: tDebug('fileDialog.logFile'), extensions: ['log', 'txt', 'ndjson'] }], - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) { updateDebugConfig({ log_path: selected }); notificationService.success(tDebug('messages.logPathUpdated'), { duration: 2000 }); diff --git a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx index 23e6d7281..24561f4cb 100644 --- a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx @@ -8,9 +8,9 @@ import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext' import { useNotification } from '@/shared/notification-system'; import { configAPI } from '../../api/service-api/ConfigAPI'; import type { SkillInfo, SkillLevel, SkillMarketItem, SkillValidationResult } from '../types'; -import { open } from '@tauri-apps/plugin-dialog'; import { createLogger } from '@/shared/utils/logger'; import './SkillsConfig.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('SkillsConfig'); @@ -176,7 +176,7 @@ const SkillsConfig: React.FC = () => { const handleBrowse = async () => { try { - const selected = await open({ directory: true, multiple: false, title: t('form.path.label') }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) setFormPath(selected as string); } catch (err) { log.error('Failed to open file dialog', err); From 6d88bb5bcf8afdae63d74d62b0e7b156cc3e74de Mon Sep 17 00:00:00 2001 From: Marqle Date: Thu, 30 Apr 2026 11:27:56 +0800 Subject: [PATCH 13/31] default terminal use bash --- .../service/terminal/src/shell/detection.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/crates/core/src/service/terminal/src/shell/detection.rs b/src/crates/core/src/service/terminal/src/shell/detection.rs index 44125b5d4..ac550bf79 100644 --- a/src/crates/core/src/service/terminal/src/shell/detection.rs +++ b/src/crates/core/src/service/terminal/src/shell/detection.rs @@ -82,6 +82,16 @@ impl ShellDetector { #[cfg(not(windows))] { + if let Some(bash_path) = Self::find_bash_with_which() { + return DetectedShell { + shell_type: ShellType::Bash, + path: bash_path.clone(), + version: Self::get_shell_version(bash_path.to_str().unwrap_or_default()), + display_name: "bash".to_string(), + }; + } else { + log::error!("bash not found"); + } // Try to use $SHELL environment variable if let Ok(shell_path) = std::env::var("SHELL") { let shell_type = ShellType::from_executable(&shell_path); @@ -303,6 +313,35 @@ impl ShellDetector { None } + #[cfg(not(windows))] + fn find_bash_with_which() -> Option { + let output = std::process::Command::new("which").arg("bash").output(); + match output { + Ok(output) => { + if output.status.success() { + let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !path_str.is_empty() { + let path = PathBuf::from(path_str); + if path.exists() { + return Some(path); + } else { + log::warn!("bash path not exist"); + } + } else { + log::warn!("bash not exist"); + } + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + log::error!("which bash error: {}", stderr.trim()); + } + } + Err(e) => { + log::error!("which bash error: {}", e); + } + }; + None + } + fn find_in_path(executable: &str) -> Option { #[cfg(windows)] let path_var = std::env::var("PATH").ok()?; From 9547b56dda4e7b4c73b7109de5ebd18c1fb3b04f Mon Sep 17 00:00:00 2001 From: Marqle Date: Tue, 28 Apr 2026 20:35:44 +0800 Subject: [PATCH 14/31] change import --- src/web-ui/src/app/layout/AppLayout.tsx | 2 +- src/web-ui/src/app/layout/panelConfig.ts | 2 +- src/web-ui/src/app/services/AppManager.ts | 2 +- src/web-ui/src/flow_chat/components/ChatInput.tsx | 2 +- src/web-ui/src/flow_chat/store/FlowChatStore.ts | 2 +- src/web-ui/src/flow_chat/store/inputHistoryStore.ts | 2 +- src/web-ui/src/hooks/useModelConfigs.ts | 2 +- src/web-ui/src/infrastructure/config/components/LspConfig.tsx | 2 +- src/web-ui/src/tools/editor/services/EditorManager.ts | 2 +- src/web-ui/src/tools/lsp/services/LspConfigService.ts | 2 +- .../src/tools/terminal/services/manualTerminalProfileService.ts | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/web-ui/src/app/layout/AppLayout.tsx b/src/web-ui/src/app/layout/AppLayout.tsx index 95a1388f1..8562105db 100644 --- a/src/web-ui/src/app/layout/AppLayout.tsx +++ b/src/web-ui/src/app/layout/AppLayout.tsx @@ -16,7 +16,7 @@ import { useApp } from '../hooks/useApp'; import { useSceneStore } from '../stores/sceneStore'; import { useShortcut } from '@/infrastructure/hooks/useShortcut'; import { configManager } from '@/infrastructure'; -import { sessionStorageAdapter } from '@/shared'; +import { sessionStorageAdapter } from '@/shared/utils/sessionStorageAdapter'; type TransitionDirection = 'entering' | 'returning' | null; import { FlowChatManager } from '../../flow_chat/services/FlowChatManager'; diff --git a/src/web-ui/src/app/layout/panelConfig.ts b/src/web-ui/src/app/layout/panelConfig.ts index 40c730f40..f5d6ab10c 100644 --- a/src/web-ui/src/app/layout/panelConfig.ts +++ b/src/web-ui/src/app/layout/panelConfig.ts @@ -13,7 +13,7 @@ * - expanded: expanded mode (wide content, more information) */ -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('PanelConfig'); diff --git a/src/web-ui/src/app/services/AppManager.ts b/src/web-ui/src/app/services/AppManager.ts index e72726408..5605083be 100644 --- a/src/web-ui/src/app/services/AppManager.ts +++ b/src/web-ui/src/app/services/AppManager.ts @@ -18,7 +18,7 @@ import { import { globalEventBus } from '../../infrastructure/event-bus'; import { createLogger } from '@/shared/utils/logger'; import { i18nService } from '@/infrastructure/i18n'; -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; const log = createLogger('AppManager'); diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index bfab815db..31ca057ab 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -58,7 +58,7 @@ import { useDeepReviewConsent } from './DeepReviewConsentDialog'; import { useSessionReviewActivity } from '../hooks/useSessionReviewActivity'; import { shouldBlockDeepReviewCommand } from '../utils/deepReviewCommandGuard'; import './ChatInput.scss'; -import { sessionStorageAdapter } from '@/shared'; +import { sessionStorageAdapter } from '@/shared/utils/sessionStorageAdapter'; const log = createLogger('ChatInput'); diff --git a/src/web-ui/src/flow_chat/store/FlowChatStore.ts b/src/web-ui/src/flow_chat/store/FlowChatStore.ts index 48cff3bba..da19b5575 100644 --- a/src/web-ui/src/flow_chat/store/FlowChatStore.ts +++ b/src/web-ui/src/flow_chat/store/FlowChatStore.ts @@ -40,7 +40,7 @@ import { import type { WorkspaceInfo } from '@/shared/types'; import { sessionBelongsToWorkspaceNavRow } from '../utils/sessionOrdering'; import { sessionMatchesWorkspace } from '../utils/workspaceScope'; -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; const log = createLogger('FlowChatStore'); diff --git a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts index 28e0012f2..90663342e 100644 --- a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts +++ b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts @@ -4,7 +4,7 @@ * History is now session-scoped - each session maintains its own input history. */ -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; import { create } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; diff --git a/src/web-ui/src/hooks/useModelConfigs.ts b/src/web-ui/src/hooks/useModelConfigs.ts index 35cc22a8f..35c5feb7d 100644 --- a/src/web-ui/src/hooks/useModelConfigs.ts +++ b/src/web-ui/src/hooks/useModelConfigs.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { ModelConfig } from '../shared/types'; import { modelConfigManager } from '../infrastructure/config/services/modelConfigs'; -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; export const useModelConfigs = () => { const [configs, setConfigs] = useState([]); const [loading, setLoading] = useState(true); diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx index 3ce480fdf..92a7d6138 100644 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx @@ -8,7 +8,7 @@ import { lspService } from '@/tools/lsp/services/LspService'; import { createLogger } from '@/shared/utils/logger'; import './LspConfig.scss'; import {workspaceAPI} from "@/infrastructure"; -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; const log = createLogger('LspConfig'); diff --git a/src/web-ui/src/tools/editor/services/EditorManager.ts b/src/web-ui/src/tools/editor/services/EditorManager.ts index a955f0c3f..da80d6a81 100644 --- a/src/web-ui/src/tools/editor/services/EditorManager.ts +++ b/src/web-ui/src/tools/editor/services/EditorManager.ts @@ -13,7 +13,7 @@ import { import { globalEventBus } from '../../../infrastructure/event-bus'; import { getMonacoLanguage } from '@/infrastructure/language-detection'; import { createLogger } from '@/shared/utils/logger'; -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; const log = createLogger('EditorManager'); diff --git a/src/web-ui/src/tools/lsp/services/LspConfigService.ts b/src/web-ui/src/tools/lsp/services/LspConfigService.ts index 520b26911..4d2dab163 100644 --- a/src/web-ui/src/tools/lsp/services/LspConfigService.ts +++ b/src/web-ui/src/tools/lsp/services/LspConfigService.ts @@ -2,7 +2,7 @@ * LSP config service (user-facing settings). */ -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('LspConfigService'); diff --git a/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts b/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts index 69d6bb534..d9f3623a8 100644 --- a/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts +++ b/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts @@ -1,4 +1,4 @@ -import { storage } from '@/shared'; +import { storage } from '@/shared/utils/storageAdapter'; import { STORAGE_KEYS } from '@/shared/constants/app'; import { createLogger } from '@/shared/utils/logger'; From d34930c8bc84727d82cc087e33da15730157174c Mon Sep 17 00:00:00 2001 From: wangchao <1398269744@qq.com> Date: Thu, 30 Apr 2026 15:16:06 +0800 Subject: [PATCH 15/31] fix log path --- src/apps/desktop/src/lib.rs | 29 ++++--------------- .../config/components/BasicsConfig.tsx | 14 +++++++-- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 2eae44307..1b4922f77 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -217,16 +217,6 @@ pub async fn _run() { } logging::register_runtime_log_state(startup_log_level, session_log_dir.clone()); - { - let candidates = ["mobile-web/dist","mobile-web","dist"]; - let mut found = false; - let path = PathBuf::from("/data/storage/el2/base/files/dist"); - if path.join("index.html").exists() { - log::info!("Found bundled mobile-web at: {}", path.display()); - api::remote_connect_api::set_mobile_web_resource_path(path); - found = true; - } - } for step in startup_timings.steps() { log::debug!( @@ -241,20 +231,13 @@ pub async fn _run() { // so the primary candidate is "mobile-web/dist". Additional fallbacks // handle legacy or non-standard bundle layouts. { - let candidates = ["mobile-web/dist", "mobile-web", "dist"]; + let candidates = ["mobile-web/dist","mobile-web","dist"]; let mut found = false; - for candidate in &candidates { - if let Ok(p) = app - .path() - .resolve(candidate, tauri::path::BaseDirectory::Resource) - { - if p.join("index.html").exists() { - log::info!("Found bundled mobile-web at: {}", p.display()); - api::remote_connect_api::set_mobile_web_resource_path(p); - found = true; - break; - } - } + let path = PathBuf::from("/data/storage/el2/base/files/dist"); + if path.join("index.html").exists() { + log::info!("Found bundled mobile-web at: {}", path.display()); + api::remote_connect_api::set_mobile_web_resource_path(path); + found = true; } if !found { // Last resort: scan the resource root for any index.html diff --git a/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx b/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx index 866551b93..d0d72156d 100644 --- a/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx @@ -263,6 +263,14 @@ function BasicsLoggingSection() { const [openingFolder, setOpeningFolder] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error' | 'info'; text: string } | null>(null); + const getFormattedLogPath = useCallback(() => { + if (!runtimeInfo?.sessionLogDir) return ''; + return runtimeInfo.sessionLogDir.replace( + '/data/storage/el2/base/files/bitfun', + '/storage/Users/currentUser/appdata/el2/base/com.huawei.bitfunide/files/bitfun' + ); + }, [runtimeInfo?.sessionLogDir]); + const levelOptions = useMemo( () => [ { value: 'trace', label: t('logging.levels.trace') }, @@ -329,7 +337,7 @@ function BasicsLoggingSection() { ); const handleOpenFolder = useCallback(async () => { - const folder = runtimeInfo?.sessionLogDir; + const folder = getFormattedLogPath(); if (!folder) { showMessage('error', t('logging.messages.pathUnavailable')); return; @@ -344,7 +352,7 @@ function BasicsLoggingSection() { } finally { setOpeningFolder(false); } - }, [runtimeInfo?.sessionLogDir, showMessage, t]); + }, [getFormattedLogPath, showMessage, t]); if (loading) { return ; @@ -380,7 +388,7 @@ function BasicsLoggingSection() { >
- {runtimeInfo?.sessionLogDir || '-'} + {getFormattedLogPath() || '-'}
- - )} - - {showMaximize && ( - - - - )} - - {showClose && ( - - - - )} -
- ); +export const WindowControls: React.FC = ( +) => { + return (
); }; diff --git a/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts b/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts index 91cafc584..183658d59 100644 --- a/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts @@ -1,4 +1,4 @@ - + import { api } from './ApiClient'; import { createTauriCommandError } from '../errors/TauriCommandError'; @@ -65,77 +65,77 @@ function groupSearchResultsByFile(results: FileSearchResult[]): FileSearchResult } export class WorkspaceAPI { - + async openWorkspace(path: string): Promise { try { - return await api.invoke('open_workspace', { - request: { path } + return await api.invoke('open_workspace', { + request: { path } }); } catch (error) { throw createTauriCommandError('open_workspace', error, { path }); } } - + async closeWorkspace(): Promise { try { - await api.invoke('close_workspace', { - request: {} + await api.invoke('close_workspace', { + request: {} }); } catch (error) { throw createTauriCommandError('close_workspace', error); } } - + async getWorkspaceInfo(): Promise { try { - return await api.invoke('get_workspace_info', { - request: {} + return await api.invoke('get_workspace_info', { + request: {} }); } catch (error) { throw createTauriCommandError('get_workspace_info', error); } } - + async listFiles(path: string): Promise { try { - return await api.invoke('list_files', { - request: { path } + return await api.invoke('list_files', { + request: { path } }); } catch (error) { throw createTauriCommandError('list_files', error, { path }); } } - + async readFile(path: string): Promise { try { - return await api.invoke('read_file', { - request: { path } + return await api.invoke('read_file', { + request: { path } }); } catch (error) { throw createTauriCommandError('read_file', error, { path }); } } - + async writeFile(path: string, content: string): Promise { try { - await api.invoke('write_file', { - request: { path, content } + await api.invoke('write_file', { + request: { path, content } }); } catch (error) { throw createTauriCommandError('write_file', error, { path, content }); } } - + async writeFileContent(workspacePath: string, filePath: string, content: string): Promise { try { - - + + await api.invoke('write_file_content', { request: { workspacePath, filePath, content } }); @@ -154,81 +154,81 @@ export class WorkspaceAPI { } } - + async createFile(path: string): Promise { try { - await api.invoke('create_file', { - request: { path } + await api.invoke('create_file', { + request: { path } }); } catch (error) { throw createTauriCommandError('create_file', error, { path }); } } - + async deleteFile(path: string): Promise { try { - await api.invoke('delete_file', { - request: { path } + await api.invoke('delete_file', { + request: { path } }); } catch (error) { throw createTauriCommandError('delete_file', error, { path }); } } - + async createDirectory(path: string): Promise { try { - await api.invoke('create_directory', { - request: { path } + await api.invoke('create_directory', { + request: { path } }); } catch (error) { throw createTauriCommandError('create_directory', error, { path }); } } - + async deleteDirectory(path: string, recursive: boolean = true): Promise { try { - await api.invoke('delete_directory', { - request: { path, recursive } + await api.invoke('delete_directory', { + request: { path, recursive } }); } catch (error) { throw createTauriCommandError('delete_directory', error, { path, recursive }); } } - + async getFileTree(path: string, maxDepth?: number): Promise { try { - return await api.invoke('get_file_tree', { - request: { path, maxDepth } + return await api.invoke('get_file_tree', { + request: { path, maxDepth } }); } catch (error) { throw createTauriCommandError('get_file_tree', error, { path, maxDepth }); } } - + async getDirectoryChildren(path: string): Promise { try { - return await api.invoke('get_directory_children', { - request: { path } + return await api.invoke('get_directory_children', { + request: { path } }); } catch (error) { throw createTauriCommandError('get_directory_children', error, { path }); } } - + async getDirectoryChildrenPaginated( - path: string, - offset: number = 0, + path: string, + offset: number = 0, limit: number = 100 ): Promise { try { - return await api.invoke('get_directory_children_paginated', { - request: { path, offset, limit } + return await api.invoke('get_directory_children_paginated', { + request: { path, offset, limit } }); } catch (error) { throw createTauriCommandError('get_directory_children_paginated', error, { path, offset, limit }); @@ -245,11 +245,11 @@ export class WorkspaceAPI { } } - + async readFileContent(filePath: string, encoding?: string): Promise { try { - return await api.invoke('read_file_content', { - request: { filePath, encoding } + return await api.invoke('read_file_content', { + request: { filePath, encoding } }); } catch (error) { throw createTauriCommandError('read_file_content', error, { filePath, encoding }); @@ -446,8 +446,8 @@ export class WorkspaceAPI { } async searchFiles( - rootPath: string, - pattern: string, + rootPath: string, + pattern: string, searchContent: boolean = true, caseSensitive: boolean = false, useRegex: boolean = false, @@ -460,10 +460,10 @@ export class WorkspaceAPI { const effectiveSearchId = searchId ?? this.createSearchId(searchContent ? 'legacy-content' : 'legacy-filenames'); try { - const resultPromise = api.invoke('search_files', { - request: { - rootPath, - pattern, + const resultPromise = api.invoke('search_files', { + request: { + rootPath, + pattern, searchContent, searchId: effectiveSearchId, caseSensitive, @@ -471,7 +471,7 @@ export class WorkspaceAPI { wholeWord, maxResults, includeDirectories, - } + } }); return await this.raceCancelable('search_files', resultPromise, effectiveSearchId, signal); @@ -494,8 +494,8 @@ export class WorkspaceAPI { } async searchFilenamesOnly( - rootPath: string, - pattern: string, + rootPath: string, + pattern: string, caseSensitive: boolean = false, useRegex: boolean = false, wholeWord: boolean = false, @@ -631,8 +631,8 @@ export class WorkspaceAPI { } async searchContentOnly( - rootPath: string, - pattern: string, + rootPath: string, + pattern: string, caseSensitive: boolean = false, useRegex: boolean = false, wholeWord: boolean = false, @@ -668,16 +668,16 @@ export class WorkspaceAPI { typeof searchIdOrSignal === 'string' ? searchIdOrSignal : this.createSearchId('content'); try { - const resultPromise = api.invoke('search_file_contents', { - request: { - rootPath, - pattern, + const resultPromise = api.invoke('search_file_contents', { + request: { + rootPath, + pattern, searchId: effectiveSearchId, caseSensitive, useRegex, wholeWord, maxResults, - } + } }); return await this.raceCancelable('search_file_contents', resultPromise, effectiveSearchId, effectiveSignal); @@ -759,11 +759,11 @@ export class WorkspaceAPI { ); } - + async renameFile(oldPath: string, newPath: string): Promise { try { - await api.invoke('rename_file', { - request: { oldPath, newPath } + await api.invoke('rename_file', { + request: { oldPath, newPath } }); } catch (error) { throw createTauriCommandError('rename_file', error, { oldPath, newPath }); @@ -789,70 +789,70 @@ export class WorkspaceAPI { async open_oh_file_dialog(): Promise { try { return await api.invoke("open_oh_file_dialog") - }catch (error){ - throw createTauriCommandError('open_oh_file_dialog',error) + } catch (error) { + throw createTauriCommandError('open_oh_file_dialog', error) } } async window_is_minimized(): Promise { try { return await api.invoke("window_is_minimized") - }catch (error){ - throw createTauriCommandError('window_is_minimized',error) + } catch (error) { + throw createTauriCommandError('window_is_minimized', error) } } async window_start_dragging(): Promise { try { return await api.invoke("window_start_dragging") - }catch (error){ - throw createTauriCommandError('window_start_dragging',error) + } catch (error) { + throw createTauriCommandError('window_start_dragging', error) } } async close_window(): Promise { try { return await api.invoke("close_window") - }catch (error){ - throw createTauriCommandError('close_window',error) + } catch (error) { + throw createTauriCommandError('close_window', error) } } async window_is_maximized(): Promise { try { return await api.invoke("window_is_maximized") - }catch (error){ - throw createTauriCommandError('window_is_maximized',error) + } catch (error) { + throw createTauriCommandError('window_is_maximized', error) } } async handle_min_window(): Promise { try { return await api.invoke("handle_min_window") - }catch (error){ - throw createTauriCommandError('handle_min_window',error) + } catch (error) { + throw createTauriCommandError('handle_min_window', error) } } async handle_max_window(): Promise { try { return await api.invoke("handle_max_window") - }catch (error){ - throw createTauriCommandError('handle_max_window',error) + } catch (error) { + throw createTauriCommandError('handle_max_window', error) } } async handle_restore_window(): Promise { try { return await api.invoke("handle_restore_window") - }catch (error){ - throw createTauriCommandError('handle_restore_window',error) + } catch (error) { + throw createTauriCommandError('handle_restore_window', error) } } async revealInExplorer(path: string): Promise { try { - await api.invoke('reveal_in_explorer', { + await api.invoke('reveal_in_explorer', { request: { path } }); } catch (error) { @@ -860,10 +860,20 @@ export class WorkspaceAPI { } } - + async setThemeMode(theme: string): Promise { + try { + await api.invoke('set_theme_mode', { + theme + }); + } catch (error) { + throw createTauriCommandError('set_theme_mode', error, { theme }); + } + } + + async startFileWatch(path: string, recursive?: boolean): Promise { try { - await api.invoke('start_file_watch', { + await api.invoke('start_file_watch', { path, recursive }); @@ -873,10 +883,10 @@ export class WorkspaceAPI { } } - + async stopFileWatch(path: string): Promise { try { - await api.invoke('stop_file_watch', { + await api.invoke('stop_file_watch', { path }); } catch (error) { @@ -885,7 +895,7 @@ export class WorkspaceAPI { } } - + async getWatchedPaths(): Promise { try { return await api.invoke('get_watched_paths', {}); @@ -894,7 +904,7 @@ export class WorkspaceAPI { } } - + async getClipboardFiles(): Promise<{ files: string[]; isCut: boolean }> { try { return await api.invoke('get_clipboard_files'); @@ -903,7 +913,7 @@ export class WorkspaceAPI { } } - + async pasteFiles( sourcePaths: string[], targetDirectory: string, diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts index dff0a194e..25ff4a810 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts @@ -14,7 +14,7 @@ import { ThemeSelectionId, } from '../types'; import { builtinThemes, getSystemPreferredDefaultThemeId } from '../presets'; -import { configAPI } from '@/infrastructure/api'; +import { configAPI, workspaceAPI } from '@/infrastructure/api'; import { monacoThemeSync } from '../integrations/MonacoThemeSync'; import { createLogger } from '@/shared/utils/logger'; @@ -258,6 +258,8 @@ export class ThemeService { this.injectCSSVariables(theme); + await workspaceAPI.setThemeMode(resolvedId); + try { monacoThemeSync.syncTheme(theme); } catch (error) { From 64de5f0a33d73879a7273810f00b1156aa9b93f1 Mon Sep 17 00:00:00 2001 From: Marqle Date: Thu, 7 May 2026 16:50:08 +0800 Subject: [PATCH 17/31] zsh adapt && remote unused permissions --- .../main/ets/entryability/EntryAbility.ets | 2 +- src/apps/vcoder/entry/src/main/module.json5 | 30 -------------- .../main/resources/base/element/string.json | 27 ++++++------ .../service/terminal/src/shell/detection.rs | 41 ++++++++++++++++++- .../src/shell/scripts/shellIntegration-rc.zsh | 28 ++++++++++++- .../config/components/BasicsConfig.tsx | 2 +- 6 files changed, 81 insertions(+), 49 deletions(-) diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets index 55d7e7191..b479a2e4f 100644 --- a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets +++ b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -126,7 +126,7 @@ export default class EntryAbility extends RustAbility { }); setTimeout(() => { windowStage.getMainWindow((err, data) => { - data.setWindowDecorHeight(32) + data.setWindowDecorHeight(44) data.setWindowDecorVisible(false); }) }, 40) diff --git a/src/apps/vcoder/entry/src/main/module.json5 b/src/apps/vcoder/entry/src/main/module.json5 index 614326843..d0827d935 100644 --- a/src/apps/vcoder/entry/src/main/module.json5 +++ b/src/apps/vcoder/entry/src/main/module.json5 @@ -34,45 +34,15 @@ } ], "requestPermissions": [ - { - "name": "ohos.permission.PREPARE_APP_TERMINATE" - }, { "name": "ohos.permission.INTERNET" }, -// { -// "name": "ohos.permission.GET_ALL_PROCESSES" -// }, { "name": "ohos.permission.READ_WRITE_USER_FILE" }, -// { -// "name": "ohos.permission.SECURE_PASTE" -// }, { "name": "ohos.permission.CUSTOM_SANDBOX" }, -// { -// "name": "ohos.permission.MANAGE_USER_IDM" -// }, -// { -// "name": "ohos.permission.ACCESS_PIN_AUTH" -// }, -// { -// "name": "ohos.permission.ACCESS_USER_AUTH_INTERNAL" -// }, -// { -// "name": "ohos.permission.DUMP" -// }, -// { -// "name": "ohos.permission.READ_DIAGNOSTIC_LOGS" -// }, -// { -// "name": "ohos.permission.READ_DFX_SYSEVENT" -// }, -// { -// "name": "ohos.permission.GET_BUNDLE_INFO_PRIVILEGED" -// }, { "name": "ohos.permission.READ_CALENDAR", "reason": "$string:module_desc", diff --git a/src/apps/vcoder/entry/src/main/resources/base/element/string.json b/src/apps/vcoder/entry/src/main/resources/base/element/string.json index f94595515..fafc54ff3 100644 --- a/src/apps/vcoder/entry/src/main/resources/base/element/string.json +++ b/src/apps/vcoder/entry/src/main/resources/base/element/string.json @@ -1,16 +1,15 @@ { - "string": [ - { - "name": "module_desc", - "value": "module description" - }, - { - "name": "EntryAbility_desc", - "value": "description" - }, - { - "name": "EntryAbility_label", - "value": "label" - } - ] + "string": [{ + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "Bitfun" + } + ] } \ No newline at end of file diff --git a/src/crates/core/src/service/terminal/src/shell/detection.rs b/src/crates/core/src/service/terminal/src/shell/detection.rs index ac550bf79..e1dbd09ae 100644 --- a/src/crates/core/src/service/terminal/src/shell/detection.rs +++ b/src/crates/core/src/service/terminal/src/shell/detection.rs @@ -82,14 +82,22 @@ impl ShellDetector { #[cfg(not(windows))] { - if let Some(bash_path) = Self::find_bash_with_which() { + if let Some(zsh_path) = Self::find_zsh_with_which() { + return DetectedShell { + shell_type: ShellType::Zsh, + path: zsh_path.clone(), + version: Self::get_shell_version(zsh_path.to_str().unwrap_or_default()), + display_name: "zsh".to_string(), + }; + } else if let Some(bash_path) = Self::find_bash_with_which() { return DetectedShell { shell_type: ShellType::Bash, path: bash_path.clone(), version: Self::get_shell_version(bash_path.to_str().unwrap_or_default()), display_name: "bash".to_string(), }; - } else { + } + { log::error!("bash not found"); } // Try to use $SHELL environment variable @@ -313,6 +321,35 @@ impl ShellDetector { None } + #[cfg(not(windows))] + fn find_zsh_with_which() -> Option { + let output = std::process::Command::new("which").arg("zsh").output(); + match output { + Ok(output) => { + if output.status.success() { + let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !path_str.is_empty() { + let path = PathBuf::from(path_str); + if path.exists() { + return Some(path); + } else { + log::warn!("zsh path not exist"); + } + } else { + log::warn!("zsh not exist"); + } + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + log::error!("which zsh error: {}", stderr.trim()); + } + } + Err(e) => { + log::error!("which zsh error: {}", e); + } + }; + None + } + #[cfg(not(windows))] fn find_bash_with_which() -> Option { let output = std::process::Command::new("which").arg("bash").output(); diff --git a/src/crates/core/src/service/terminal/src/shell/scripts/shellIntegration-rc.zsh b/src/crates/core/src/service/terminal/src/shell/scripts/shellIntegration-rc.zsh index 871a0dbd7..99b13327d 100644 --- a/src/crates/core/src/service/terminal/src/shell/scripts/shellIntegration-rc.zsh +++ b/src/crates/core/src/service/terminal/src/shell/scripts/shellIntegration-rc.zsh @@ -1,7 +1,33 @@ # --------------------------------------------------------------------------------------------- # Shell Integration for Zsh # --------------------------------------------------------------------------------------------- -builtin autoload -Uz add-zsh-hook is-at-least + +add-zsh-hook() { + local hook_name= "$1" + local func_name= "$2" + local -a hook_array + + case "${hook_name}" in + precmd) + hook_array=("${(@)precmd_functions[@]}") + precmd_functions+=("${func_name}") + ;; + preexec) + preexec_functions+=("${func_name}") + ;; + *) + return 1 + ;; + esac +} + +if ! builtin type is-at-least >/dev/null 2>&1; then + is-at-least() { + local required="$1" + local current="${ZSH_VERSION}" + [[ "${current}" == $(echo -e "${required}\n${current}" sort -V | head -n1) ]] + } +fi # Prevent the script recursing when setting up if [ -n "$TERMINAL_SHELL_INTEGRATION" ]; then diff --git a/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx b/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx index d0d72156d..e6e30c4e7 100644 --- a/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx @@ -267,7 +267,7 @@ function BasicsLoggingSection() { if (!runtimeInfo?.sessionLogDir) return ''; return runtimeInfo.sessionLogDir.replace( '/data/storage/el2/base/files/bitfun', - '/storage/Users/currentUser/appdata/el2/base/com.huawei.bitfunide/files/bitfun' + '/storage/Users/currentUser/appdata/el2/base/com.huawei.BitFun/files/bitfun' ); }, [runtimeInfo?.sessionLogDir]); From a283fa841bf1900e27b1d540159d92ee8367fab5 Mon Sep 17 00:00:00 2001 From: wangchao <1398269744@qq.com> Date: Thu, 7 May 2026 11:19:34 +0800 Subject: [PATCH 18/31] fix knock share issue. --- .../desktop/src/api/remote_connect_api.rs | 6 +++ src/apps/desktop/src/lib.rs | 1 + .../main/ets/entryability/EntryAbility.ets | 40 ++++++++++++++----- .../core/src/service/remote_connect/mod.rs | 36 +++++++++++++++++ .../RemoteConnectDialog.tsx | 3 +- .../api/service-api/RemoteConnectAPI.ts | 8 ++++ 6 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 83cb1aeab..f6a607ccc 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -421,6 +421,12 @@ pub async fn remote_connect_stop() -> Result<(), String> { Ok(()) } +#[tauri::command] +pub async fn send_remote_connect_dialog_status(is_open: bool) -> Result<(), String> { + bitfun_core::service::remote_connect::send_remote_dialog_status(is_open); + Ok(()) +} + #[tauri::command] pub async fn remote_connect_stop_bot() -> Result<(), String> { let holder = get_service_holder(); diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 536dcc56a..014ed14af 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -700,6 +700,7 @@ pub async fn _run() { api::remote_connect_api::remote_connect_start, api::remote_connect_api::remote_connect_stop, api::remote_connect_api::remote_connect_stop_bot, + api::remote_connect_api::send_remote_connect_dialog_status, api::remote_connect_api::remote_connect_status, api::remote_connect_api::remote_connect_get_form_state, api::remote_connect_api::remote_connect_set_form_state, diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets index b479a2e4f..55abe1ba8 100644 --- a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets +++ b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -26,12 +26,12 @@ export default class EntryAbility extends RustAbility { public moduleName: string = "bitfun_desktop_lib"; public defaultPage: boolean = true; public commonEventListener: CommonEventListener | undefined = undefined; - public remote_url: string = ""; + public remoteUrl: string = ""; + public shareStatus: boolean = false; async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise { super.onCreate(want, launchParam); this.commonEventListener = new CommonEventListener(); - this.aboutToAppear(); } onDestroy(): void { @@ -105,9 +105,25 @@ export default class EntryAbility extends RustAbility { }); RustModule.registerArktsFunction('send_remote_url', async (err: Error, arg: string): Promise => { hilog.info(DOMAIN_NUMBER, TAG, 'get remote url ' + arg); - this.remote_url = arg; + this.remoteUrl = arg; + if (this.remoteUrl.length == 0) { + this.shareDisablingListening(); + } + else { + this.shareListening(); + } return ''; }); + RustModule.registerArktsFunction('send_remote_dialog_status', async (err: Error, arg: string): Promise => { + hilog.info(DOMAIN_NUMBER, TAG, 'get remote dialog status ' + arg); + if (arg.length == 0) { + this.shareDisablingListening(); + } + else { + this.shareListening(); + } + return ''; + }); RustModule.registerArktsFunction('harmony_create', async (err: Error, arg: string): Promise => { await fileIo.copyDir('/storage/Users/currentUser/Documents/DevecoStudioProjects/MyApplication', '/storage/Users/currentUser/Documents/files', 1).then(() => { @@ -158,14 +174,20 @@ export default class EntryAbility extends RustAbility { return super.onWindowStageCreate(windowStage); } - aboutToAppear(): void { - console.info("aboutToAppear"); - harmonyShare.on('knockShare', this.sendOnlyCallback); + private shareListening() { + hilog.info(0x0000, 'vnext', 'shareListening'); + if (this.remoteUrl.length !=0 || !this.shareStatus) { + harmonyShare.on('knockShare', this.sendOnlyCallback); + this.shareStatus = true; + } + } + private shareDisablingListening() { + harmonyShare.off('knockShare', this.sendOnlyCallback); + this.shareStatus = false; } - private sendOnlyCallback = (sharableTable: harmonyShare.SharableTarget) => { - if (this.remote_url.length == 0) { - let content = this.remote_url; + if (this.remoteUrl.length == 0) { + let content = this.remoteUrl; let shareData: systemShare.SharedData = new systemShare.SharedData({ utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, content, diff --git a/src/crates/core/src/service/remote_connect/mod.rs b/src/crates/core/src/service/remote_connect/mod.rs index 9a4465a15..5f830ef12 100644 --- a/src/crates/core/src/service/remote_connect/mod.rs +++ b/src/crates/core/src/service/remote_connect/mod.rs @@ -953,6 +953,7 @@ impl RemoteConnectService { self.pairing.write().await.reset().await; *self.trusted_mobile_identity.write().await = None; + let _ = send_remote_url(String::new()); info!("Relay connections stopped (bots unaffected)"); } @@ -1384,4 +1385,39 @@ fn send_remote_url(args: String) -> Result { Ok(res) } } +} +pub fn send_remote_dialog_status(is_open: bool) -> Result { + let args = if is_open { + "is_open".to_owned() + } + else { + String::new() + }; + let result = Ok(args); + let results = Arc::new(Mutex::new(String::default())); + match JS_THREADSAFE_FUNCTION.write().get("send_remote_dialog_status") { + None => { + log::error!("send_remote_dialog_status has not register"); + Err("The Arkts has not register the function".to_owned()) + } + Some(function) => { + function.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result, _| { + match result { + Ok(_) => { + log::info!("send_remote_dialog_status successfully"); + } + Err(err) => { + log::error!("send_remote_dialog_status failed with error: {}", err); + } + } + Ok(()) + }, + ); + let res = results.lock().to_string(); + Ok(res) + } + } } \ No newline at end of file diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx index dff963203..c6d291c99 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx +++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.tsx @@ -173,10 +173,11 @@ export const RemoteConnectDialog: React.FC = ({ useEffect(() => { if (!isOpen) { if (pollRef.current) clearInterval(pollRef.current); + void remoteConnectAPI.sendRemoteDialogStatus(false); pollRef.current = null; return; } - + void remoteConnectAPI.sendRemoteDialogStatus(true); setHasAgreedDisclaimer(getRemoteConnectDisclaimerAgreed()); let cancelled = false; diff --git a/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts b/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts index b30d27a90..5bda6708b 100644 --- a/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts @@ -135,6 +135,14 @@ class RemoteConnectAPIService { throw e; } } + async sendRemoteDialogStatus(is_open: boolean): Promise { + try { + await this.adapter.request('send_remote_connect_dialog_status', {isOpen: is_open}); + } catch (e) { + log.error('sendRemoteDialogStatus failed', e); + throw e; + } + } async getStatus(): Promise { try { From 1fd08f2d6fa772fbaadd1bd8849976a37633fb4e Mon Sep 17 00:00:00 2001 From: Marqle Date: Fri, 8 May 2026 09:38:49 +0800 Subject: [PATCH 19/31] adapt window move --- .../main/ets/entryability/EntryAbility.ets | 3 +- .../src/app/components/NavBar/NavBar.tsx | 44 +++++++++---------- .../src/app/components/SceneBar/SceneBar.tsx | 34 +++++++------- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets index 55abe1ba8..5d4c850fb 100644 --- a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets +++ b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -142,7 +142,8 @@ export default class EntryAbility extends RustAbility { }); setTimeout(() => { windowStage.getMainWindow((err, data) => { - data.setWindowDecorHeight(44) + data.setWindowDecorHeight(44); + data.setWindowTitleMoveEnabled(false); data.setWindowDecorVisible(false); }) }, 40) diff --git a/src/web-ui/src/app/components/NavBar/NavBar.tsx b/src/web-ui/src/app/components/NavBar/NavBar.tsx index a9e08cffe..4ffcc1fdc 100644 --- a/src/web-ui/src/app/components/NavBar/NavBar.tsx +++ b/src/web-ui/src/app/components/NavBar/NavBar.tsx @@ -16,7 +16,7 @@ import { useNavSceneStore } from '../../stores/navSceneStore'; import { useI18n } from '../../../infrastructure/i18n'; import { PanelLeftIcon } from '../TitleBar/PanelIcons'; import { createLogger } from '@/shared/utils/logger'; -import { isMacOSDesktopRuntime, supportsNativeWindowDragging } from '@/infrastructure/runtime'; +import { isMacOSDesktopRuntime } from '@/infrastructure/runtime'; import './NavBar.scss'; import { workspaceAPI } from '@/infrastructure'; @@ -42,36 +42,32 @@ const NavBar: React.FC = ({ const isMacOS = useMemo(() => { return isMacOSDesktopRuntime(); }, []); - const canDragWindow = supportsNativeWindowDragging(); const showSceneNav = useNavSceneStore(s => s.showSceneNav); - const navSceneId = useNavSceneStore(s => s.navSceneId); - const goBack = useNavSceneStore(s => s.goBack); - const goForward = useNavSceneStore(s => s.goForward); - const canGoBack = showSceneNav && !!navSceneId; + const navSceneId = useNavSceneStore(s => s.navSceneId); + const goBack = useNavSceneStore(s => s.goBack); + const goForward = useNavSceneStore(s => s.goForward); + const canGoBack = showSceneNav && !!navSceneId; const canGoForward = !showSceneNav && !!navSceneId; - const lastMouseDownTimeRef = useRef(0); + const isDraggingRef = useRef(false); - const handleBarMouseDown = useCallback((e: React.MouseEvent) => { - if (!canDragWindow) return; + const handleBarMouseDown = (() => { + isDraggingRef.current = true; + }) - const now = Date.now(); - const timeSinceLastMouseDown = now - lastMouseDownTimeRef.current; - lastMouseDownTimeRef.current = now; - - if (e.button !== 0) return; - const target = e.target as HTMLElement | null; - if (!target) return; - if (target.closest(INTERACTIVE_SELECTOR)) return; - if (timeSinceLastMouseDown < 500 && timeSinceLastMouseDown > 50) return; - - void (async () => { + const handleBarMouseMove = async () => { + if (isDraggingRef.current) { try { await workspaceAPI.window_start_dragging(); } catch (error) { log.debug('startDragging failed', error); } - })(); - }, [canDragWindow]); + } + + }; + + const handlebarMouseUp = (() => { + isDraggingRef.current = false; + }); const handleBarDoubleClick = useCallback((e: React.MouseEvent) => { const target = e.target as HTMLElement | null; @@ -84,7 +80,7 @@ const NavBar: React.FC = ({ if (isCollapsed) { return ( -
+
+ · + +

{license.text}

{t('about.copyright')} @@ -117,6 +207,135 @@ export const AboutDialog: React.FC = ({

+ + {/* Open Source Software dialog */} + setSubDialog(null)} + title={t('about.openSource')} + showCloseButton={true} + size="medium" + > +
+

+ {t('about.openSourceDesc')} +

+ +
+
+
+

Frontend

+ + {dependencies.filter(d => d.category === 'frontend').length} + +
+
+ {dependencies.filter(d => d.category === 'frontend').map((dep) => ( +
+
+ +
+
+ + + {dep.license} + +
+ + FE + +
+ ))} +
+
+
+ +
+
+
+

Backend

+ + {dependencies.filter(d => d.category === 'backend').length} + +
+
+ {dependencies.filter(d => d.category === 'backend').map((dep) => ( +
+
+ +
+
+ + + {dep.license} + +
+ + BE + +
+ ))} +
+
+
+ +

+ {t('about.openSourceFootnote')} +

+
+
+ + {/* User Agreement dialog */} + setSubDialog(null)} + title={t('about.userAgreement')} + showCloseButton={true} + size="medium" + > +
+
+

+ 1. 服务使用 +

+

+ 用户在使用 BitFun 服务时应遵守相关法律法规。本软件仅供合法用途使用,不得用于任何非法或未经授权的活动。 +

+
+
+

+ 2. 免责声明 +

+

+ 本软件按"现状"提供,不提供任何明示或暗示的保证。在适用法律允许的最大范围内,开发者不承担任何损害赔偿的责任。使用风险由用户自行承担。 +

+
+
+

+ 3. 隐私政策 +

+

+ 我们重视你的隐私。本软件可能会收集必要的使用数据以改善服务质量。详细的隐私政策请参阅官方网站。 +

+
+

+ 完整协议内容将在后续版本中完善。 +

+
+
+ ); }; diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index f06f39b19..67f545cbb 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -484,7 +484,11 @@ "buildDate": "Build Date", "commit": "Commit", "branch": "Branch", - "copyright": "© 2025 BitFun. All rights reserved." + "copyright": "© 2025 BitFun. All rights reserved.", + "openSource": "Open Source Software", + "openSourceDesc": "BitFun uses the following open source components:", + "openSourceFootnote": "Click a component name to open its official page for more information.", + "userAgreement": "User Agreement" }, "actions": { "confirm": "Confirm", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index c61824a40..9b0e22184 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -484,7 +484,11 @@ "buildDate": "构建日期", "commit": "提交", "branch": "分支", - "copyright": "© 2025 BitFun. All rights reserved." + "copyright": "© 2025 BitFun. All rights reserved.", + "openSource": "开源软件", + "openSourceDesc": "BitFun 使用了以下开源组件:", + "openSourceFootnote": "点击组件名称可打开其官方页面获取更多信息。", + "userAgreement": "用户协议" }, "actions": { "confirm": "确认", diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 44c4444fb..28502c724 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -484,7 +484,11 @@ "buildDate": "構建日期", "commit": "提交", "branch": "分支", - "copyright": "© 2025 BitFun. All rights reserved." + "copyright": "© 2025 BitFun. All rights reserved.", + "openSource": "開源軟件", + "openSourceDesc": "BitFun 使用了以下開源組件:", + "openSourceFootnote": "點擊組件名稱可打開其官方頁面獲取更多資訊。", + "userAgreement": "用戶協議" }, "actions": { "confirm": "確認", From 08cc0901c5e94ee26468875da3fcd46e3ffa944d Mon Sep 17 00:00:00 2001 From: SWangHash <88996709+SWangHash@users.noreply.github.com> Date: Tue, 12 May 2026 15:09:04 +0800 Subject: [PATCH 22/31] fix knock share issue. --- .../vcoder/entry/src/main/ets/entryability/EntryAbility.ets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets index 606b5c592..15acc7539 100644 --- a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets +++ b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -192,7 +192,7 @@ export default class EntryAbility extends RustAbility { private shareListening() { hilog.info(0x0000, 'vnext', 'shareListening'); - if (this.remoteUrl.length !=0 || !this.shareStatus) { + if (this.remoteUrl.length != 0 && !this.shareStatus) { harmonyShare.on('knockShare', this.sendOnlyCallback); this.shareStatus = true; } @@ -202,7 +202,7 @@ export default class EntryAbility extends RustAbility { this.shareStatus = false; } private sendOnlyCallback = (sharableTable: harmonyShare.SharableTarget) => { - if (this.remoteUrl.length == 0) { + if (this.remoteUrl.length != 0) { let content = this.remoteUrl; let shareData: systemShare.SharedData = new systemShare.SharedData({ utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, From bda3aee9fc503450e91acbbe6808cb047465b847 Mon Sep 17 00:00:00 2001 From: Marqle Date: Wed, 13 May 2026 09:31:24 +0800 Subject: [PATCH 23/31] use bash first --- src/apps/desktop/src/api/terminal_api.rs | 5 +++-- .../service/terminal/src/shell/detection.rs | 20 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/apps/desktop/src/api/terminal_api.rs b/src/apps/desktop/src/api/terminal_api.rs index 4995952d9..24962acde 100644 --- a/src/apps/desktop/src/api/terminal_api.rs +++ b/src/apps/desktop/src/api/terminal_api.rs @@ -1,5 +1,6 @@ //! Terminal API +use bitfun_core::infrastructure::PathManager; use log::{error, warn}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -72,8 +73,8 @@ impl TerminalState { /// Get the scripts directory path for shell integration /// Uses the same path structure as PathManager fn get_scripts_dir() -> PathBuf { - dirs::config_dir() - .unwrap_or_else(|| PathBuf::from(".")) + PathManager::new().map(|p| p.home_dir()) + .unwrap_or_else(|_| PathBuf::from(".")) .join("bitfun") .join("temp") .join("scripts") diff --git a/src/crates/core/src/service/terminal/src/shell/detection.rs b/src/crates/core/src/service/terminal/src/shell/detection.rs index e1dbd09ae..4b3557399 100644 --- a/src/crates/core/src/service/terminal/src/shell/detection.rs +++ b/src/crates/core/src/service/terminal/src/shell/detection.rs @@ -82,22 +82,21 @@ impl ShellDetector { #[cfg(not(windows))] { - if let Some(zsh_path) = Self::find_zsh_with_which() { - return DetectedShell { - shell_type: ShellType::Zsh, - path: zsh_path.clone(), - version: Self::get_shell_version(zsh_path.to_str().unwrap_or_default()), - display_name: "zsh".to_string(), - }; - } else if let Some(bash_path) = Self::find_bash_with_which() { + if let Some(bash_path) = Self::find_bash_with_which() { return DetectedShell { shell_type: ShellType::Bash, path: bash_path.clone(), version: Self::get_shell_version(bash_path.to_str().unwrap_or_default()), display_name: "bash".to_string(), }; - } - { + } else if let Some(zsh_path) = Self::find_zsh_with_which() { + return DetectedShell { + shell_type: ShellType::Zsh, + path: zsh_path.clone(), + version: Self::get_shell_version(zsh_path.to_str().unwrap_or_default()), + display_name: "zsh".to_string(), + }; + } else { log::error!("bash not found"); } // Try to use $SHELL environment variable @@ -304,6 +303,7 @@ impl ShellDetector { PathBuf::from(format!("/usr/local/bin/{}", executable)), PathBuf::from(format!("/usr/bin/{}", executable)), PathBuf::from(format!("/bin/{}", executable)), + PathBuf::from(format!("/data/service/hnp/bin/{}", executable)), ]; for path in candidates { From bafc7b06efc516e44038872578ed4b4f2417742e Mon Sep 17 00:00:00 2001 From: SWangHash <88996709+SWangHash@users.noreply.github.com> Date: Wed, 13 May 2026 10:41:24 +0800 Subject: [PATCH 24/31] built-in remote resources --- src/apps/desktop/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 81534d63b..809c9e6e3 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -396,7 +396,7 @@ pub async fn _run() { { let candidates = ["mobile-web/dist", "mobile-web", "dist"]; let mut found = false; - let path = PathBuf::from("/data/storage/el2/base/files/dist"); + let path = PathBuf::from("/data/storage/el1/bundle/entry/resources/resfile/dist"); if path.join("index.html").exists() { log::info!("Found bundled mobile-web at: {}", path.display()); api::remote_connect_api::set_mobile_web_resource_path(path); From 95f441cfcbb3b6c2cf350b38fd2ee116354ec67e Mon Sep 17 00:00:00 2001 From: SWangHash <88996709+SWangHash@users.noreply.github.com> Date: Thu, 14 May 2026 19:59:31 +0800 Subject: [PATCH 25/31] feat: update privacy agreement, format skill path display --- src/apps/vcoder/AppScope/app.json5 | 2 +- .../main/ets/entryability/EntryAbility.ets | 2 +- src/apps/vcoder/entry/src/main/module.json5 | 2 +- .../main/resources/base/element/string.json | 2 +- .../main/resources/base/media/bitfun_icon.png | Bin 0 -> 17294 bytes .../components/AboutDialog/AboutDialog.scss | 58 +++++------ .../components/AboutDialog/AboutDialog.tsx | 93 ++++++++---------- .../src/app/scenes/skills/SkillsScene.scss | 6 ++ .../src/app/scenes/skills/SkillsScene.tsx | 38 ++++--- .../config/components/SkillsConfig.tsx | 9 +- src/web-ui/src/locales/en-US/common.json | 26 +++++ src/web-ui/src/locales/zh-CN/common.json | 26 +++++ src/web-ui/src/locales/zh-TW/common.json | 26 +++++ 13 files changed, 186 insertions(+), 104 deletions(-) create mode 100644 src/apps/vcoder/entry/src/main/resources/base/media/bitfun_icon.png diff --git a/src/apps/vcoder/AppScope/app.json5 b/src/apps/vcoder/AppScope/app.json5 index 7b14e5ef5..e70e69703 100644 --- a/src/apps/vcoder/AppScope/app.json5 +++ b/src/apps/vcoder/AppScope/app.json5 @@ -5,7 +5,7 @@ "versionCode": 1000000, "versionName": "1.0.0", "buildVersion": "1", - "icon": "$media:layered_image", + "icon": "$media:bitfun_icon", "label": "$string:app_name" } } diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets index 15acc7539..a0ab8497e 100644 --- a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets +++ b/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets @@ -207,7 +207,7 @@ export default class EntryAbility extends RustAbility { let shareData: systemShare.SharedData = new systemShare.SharedData({ utd: uniformTypeDescriptor.UniformDataType.HYPERLINK, content, - title: "Bitfun", + title: "BitFun", description: "Phone", }); sharableTable.share(shareData) diff --git a/src/apps/vcoder/entry/src/main/module.json5 b/src/apps/vcoder/entry/src/main/module.json5 index d0827d935..5d753164e 100644 --- a/src/apps/vcoder/entry/src/main/module.json5 +++ b/src/apps/vcoder/entry/src/main/module.json5 @@ -16,7 +16,7 @@ "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "$string:EntryAbility_desc", - "icon": "$media:layered_image", + "icon": "$media:bitfun_icon", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background", diff --git a/src/apps/vcoder/entry/src/main/resources/base/element/string.json b/src/apps/vcoder/entry/src/main/resources/base/element/string.json index fafc54ff3..fc8fbb64e 100644 --- a/src/apps/vcoder/entry/src/main/resources/base/element/string.json +++ b/src/apps/vcoder/entry/src/main/resources/base/element/string.json @@ -9,7 +9,7 @@ }, { "name": "EntryAbility_label", - "value": "Bitfun" + "value": "BitFun" } ] } \ No newline at end of file diff --git a/src/apps/vcoder/entry/src/main/resources/base/media/bitfun_icon.png b/src/apps/vcoder/entry/src/main/resources/base/media/bitfun_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..29f7221d0091a03c983524e25ac77ddf200ad249 GIT binary patch literal 17294 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 literal 0 HcmV?d00001 diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss b/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss index e5ba8e5ce..622b15f16 100644 --- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss +++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss @@ -640,50 +640,42 @@ } } -// ==================== User agreement card sections ==================== +// ==================== Privacy document layout ==================== -.bitfun-about-dialog__sub-card { - display: flex; - flex-direction: column; - gap: 8px; - padding: 14px 16px; - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.03) 0%, - rgba(255, 255, 255, 0.01) 100% - ); - border: 1px solid var(--border-subtle); - border-left: 2px solid var(--color-accent-400); - border-radius: 8px; - transition: all 0.2s ease; +.bitfun-about-dialog__privacy-doc { + padding: 24px 28px 28px; + max-height: min(520px, calc(100vh - 200px)); + overflow-y: auto; + line-height: 1.8; +} - &:hover { - background: linear-gradient(135deg, - rgba(59, 130, 246, 0.06) 0%, - rgba(139, 92, 246, 0.03) 100% - ); - border-color: rgba(59, 130, 246, 0.2); - border-left-color: var(--color-accent-500); - box-shadow: - 0 4px 12px rgba(96, 165, 250, 0.08), - 0 2px 4px rgba(0, 0, 0, 0.08); - } +.bitfun-about-dialog__privacy-title { + font-size: 16px; + font-weight: 700; + color: var(--color-text-primary); + margin: 0 0 16px 0; + line-height: 1.4; + text-align: center; } -.bitfun-about-dialog__sub-card-heading { +.bitfun-about-dialog__privacy-section { font-size: 13px; font-weight: 600; color: var(--color-text-primary); - margin: 0; - display: flex; - align-items: center; - gap: 8px; + margin: 16px 0 6px 0; + line-height: 1.5; + + &:first-of-type { + margin-top: 0; + } } -.bitfun-about-dialog__sub-card-text { +.bitfun-about-dialog__privacy-text { font-size: 12px; color: var(--color-text-secondary); - line-height: 1.7; - margin: 0; + margin: 0 0 6px 0; + line-height: 1.8; + text-align: justify; } .bitfun-about-dialog__sub-footnote { diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx index d4b74f63a..c820a324d 100644 --- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx +++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx @@ -405,7 +405,7 @@ export const AboutDialog: React.FC = ({
-

Frontend

+

{t('about.openSourceFrontend')}

{dependencies.filter(d => d.category === 'frontend').length} @@ -429,7 +429,7 @@ export const AboutDialog: React.FC = ({
- FE + {t('about.openSourceTagFE')}
))} @@ -440,7 +440,7 @@ export const AboutDialog: React.FC = ({
-

Backend

+

{t('about.openSourceBackend')}

{dependencies.filter(d => d.category === 'backend').length} @@ -464,7 +464,7 @@ export const AboutDialog: React.FC = ({
- BE + {t('about.openSourceTagBE')}
))} @@ -478,53 +478,46 @@ export const AboutDialog: React.FC = ({
- {/* User Agreement dialog */} - setSubDialog(null)} - title={t('about.userAgreement')} - showCloseButton={true} - size="medium" - > -
-
-

- 1. 服务使用 -

-

- 用户在使用 BitFun 服务时应遵守相关法律法规。本软件仅供合法用途使用,不得用于任何非法或未经授权的活动。 -

-
-
-

- 2. 免责声明 -

-

- 本软件按"现状"提供,不提供任何明示或暗示的保证。在适用法律允许的最大范围内,开发者不承担任何损害赔偿的责任。使用风险由用户自行承担。 -

-
-
-

- 3. 隐私政策 -

-

- 我们重视你的隐私。本软件可能会收集必要的使用数据以改善服务质量。详细的隐私政策请参阅官方网站。 -

-
-

- 完整协议内容将在后续版本中完善。 -

-
-
+ {/* Privacy Agreement dialog */} + setSubDialog(null)} + title={t('about.userAgreement')} + showCloseButton={true} + size="medium" + > +
+

{t('about.privacyTitle')}

+

{t('about.privacyIntro')}

+

{t('about.privacyCommitment')}

+ +

{t('about.privacyS1Title')}

+

{t('about.privacyS1P1')}

+

{t('about.privacyS1P2')}

+

{t('about.privacyS1P3')}

+ +

{t('about.privacyS2Title')}

+

{t('about.privacyS2P1')}

+ +

{t('about.privacyS3Title')}

+

{t('about.privacyS3P1')}

+

{t('about.privacyS3P2')}

+ +

{t('about.privacyS4Title')}

+

{t('about.privacyS4P1')}

+

{t('about.privacyS4P2')}

+

{t('about.privacyS4P3')}

+
+
- - + + ); }; diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.scss b/src/web-ui/src/app/scenes/skills/SkillsScene.scss index 47d3a4262..ab9574706 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.scss +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.scss @@ -1064,6 +1064,12 @@ &__detail-link { text-decoration: underline; text-underline-offset: 2px; + background: none; + border: none; + padding: 0; + cursor: pointer; + font: inherit; + text-align: left; } &__path-input { diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx index 21de8d894..e8121e619 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.tsx +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.tsx @@ -23,6 +23,7 @@ import { Badge, Button, ConfirmDialog, Input, Modal, Search, Select } from '@/co import { GalleryDetailModal } from '@/app/components'; import type { SkillInfo, SkillLevel, SkillMarketItem } from '@/infrastructure/config/types'; import { workspaceAPI } from '@/infrastructure/api'; +import { systemAPI } from '@/infrastructure/api'; import { workspaceManager } from '@/infrastructure/services/business/workspaceManager'; import { useNotification } from '@/shared/notification-system'; import { isRemoteWorkspace } from '@/shared/types'; @@ -38,6 +39,12 @@ import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefre const log = createLogger('SkillsScene'); +function formatDisplayPath(path: string): string { + return path.replace( + '/data/storage/el2/base/files/bitfun', + '/storage/Users/currentUser/appdata/el2/base/com.huawei.BitFun/files/bitfun' + ); +} type SkillTab = 'installed' | 'discover'; const INSTALLED_PAGE_SIZE = 12; @@ -667,12 +674,12 @@ const SkillsScene: React.FC = () => { type="button" className="bitfun-skills-scene__detail-path-btn" title={t('list.item.openPathInExplorer')} - onClick={() => void handleRevealSkillPath(selectedInstalledSkill.path)} + onClick={() => void handleRevealSkillPath(formatDisplayPath(selectedInstalledSkill.path))} > - {selectedInstalledSkill.path} + {formatDisplayPath(selectedInstalledSkill.path)} ) : ( - {selectedInstalledSkill.path} + {formatDisplayPath(selectedInstalledSkill.path)} )}
@@ -692,19 +699,18 @@ const SkillsScene: React.FC = () => { ) : null} - {selectedMarketSkill?.url ? ( -
- ) : null} + {selectedMarketSkill?.url ? ( +
+ {t('market.detail.linkLabel')} + +
+ ) : null} { const { t } = useTranslation('settings/skills'); const [showAddForm, setShowAddForm] = useState(false); @@ -299,7 +306,7 @@ const SkillsConfig: React.FC = () => {
{skill.description}
{t('list.item.pathLabel')} - {skill.path} + {formatDisplayPath(skill.path)}
); diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 3c585d1f7..2c8431ba3 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -486,6 +486,32 @@ "commit": "Commit", "branch": "Branch", "copyright": "© 2025 BitFun. All rights reserved.", + "openSource": "Open Source Software", + "openSourceDesc": "BitFun uses the following open source components:", + "openSourceFootnote": "Click a component name to open its official page for more information.", + "userAgreement": "Privacy Agreement", + "openSourceFrontend": "Frontend", + "openSourceBackend": "Backend", + "openSourceTagFE": "FE", + "openSourceTagBE": "BE", + "privacyTitle": "Privacy Statement Regarding BitFun", + "privacyUpdated": "Updated: 2026.5.15", + "privacyIntro": "BitFun is provided by Hangzhou Huawei Enterprise Communication Technology Co., Ltd. (hereinafter referred to as \"we\") as an AI-assisted coding and development productivity application. This privacy statement is established by us to govern the processing of your personal information.", + "privacyCommitment": "We value your personal information and privacy protection, and will provide appropriate security measures for your personal information in accordance with legal requirements and industry-standard security practices.", + "privacyS1Title": "1. How We Collect and Use Your Personal Information", + "privacyS1P1": "We only use your personal information when there is a legal basis to do so. Under applicable law, we may process your personal information based on your consent, as necessary for performing or entering into a contract with you, or as required to comply with legal obligations.", + "privacyS1P2": "1.1 Based on legal obligations or other circumstances prescribed by laws and regulations, we may process the following personal information:", + "privacyS1P3": "To implement application functions, we need to collect your commonly used instructions after obtaining your consent.", + "privacyS2Title": "2. Managing Your Personal Information", + "privacyS2P1": "If you have further requirements regarding your data subject rights, or have any questions, comments, or suggestions, please contact us through the methods described in the \"How to Contact Us\" section of this statement to exercise your relevant rights.", + "privacyS3Title": "3. Storage Location and Duration", + "privacyS3P1": "3.1 We commit that, unless otherwise required by laws and regulations, the retention period for your information shall be the minimum period necessary to achieve the processing purpose.", + "privacyS3P2": "3.2 The above information will be transmitted and stored on the local server.", + "privacyS4Title": "4. How to Contact Us", + "privacyS4P1": "You may contact us through the following methods to exercise your relevant rights, and we will respond as soon as possible.", + "privacyS4P2": "Address: Building 1, No. 410 Jianghong Road, Changhe Street, Binjiang District, Hangzhou, Zhejiang Province, China", + "privacyS4P3": "If you are not satisfied with our response, especially if the processing of personal information infringes upon your legitimate rights and interests, you may also seek resolution through external channels such as filing a lawsuit with a competent court or filing a complaint with industry self-regulatory organizations or relevant government authorities. You may also inquire with us about potentially applicable complaint channels.", + "privacyEffective": "Effective: 2026.5.15", "updateSectionTitle": "App updates", "updateSectionHint": "Keep BitFun up to date for the latest fixes and features." }, diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index f7750cd39..cc84b8002 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -486,6 +486,32 @@ "commit": "提交", "branch": "分支", "copyright": "© 2025 BitFun. All rights reserved.", + "openSource": "开源软件", + "openSourceDesc": "BitFun 使用了以下开源组件:", + "openSourceFootnote": "点击组件名称可打开其官方页面获取更多信息。", + "userAgreement": "隐私协议", + "openSourceFrontend": "前端", + "openSourceBackend": "后端", + "openSourceTagFE": "前端", + "openSourceTagBE": "后端", + "privacyTitle": "关于 BitFun 与隐私的声明", + "privacyUpdated": "更新日期:2026.5.15", + "privacyIntro": "BitFun 是由 杭州华为企业通信技术有限公司 (以下简称\u201C我们\u201D)为您提供的,用于提供 AI 辅助编程与开发效率提升服务的应用。本隐私声明由我们为处理您的个人信息而制定。", + "privacyCommitment": "我们非常重视您的个人信息和隐私保护,将会按照法律要求和业界成熟的安全标准,为您的个人信息提供相应的安全保护措施。", + "privacyS1Title": "1. 我们如何收集和使用您的个人信息", + "privacyS1P1": "我们仅在有合法性基础的情形下才会使用您的个人信息。根据适用的法律,我们可能会基于您的同意、为履行/订立您与我们的合同所必需、履行法定义务所必需等合法性基础,使用您的个人信息。", + "privacyS1P2": "1.1 基于履行法定义务或其他法律法规规定的情形,我们可能会处理您的以下个人信息:", + "privacyS1P3": "为了实现应用功能,在获取您的同意后我们需要收集您的常用指令。", + "privacyS2Title": "2. 管理您的个人信息", + "privacyS2P1": "如您对您的数据主体权利有进一步要求或存在任何疑问、意见或建议,可通过本声明中\u201C如何联系我们\u201D章节中所述方式与我们取得联系,并行使您的相关权利。", + "privacyS3Title": "3. 信息存储地点及期限", + "privacyS3P1": "3.1 我们承诺,除法律法规另有规定外,我们对您的信息的保存期限应当为实现处理目的所必要的最短时间。", + "privacyS3P2": "3.2 上述信息将会传输并保存至本地的服务器。", + "privacyS4Title": "4. 如何联系我们", + "privacyS4P1": "您可通过以下方式联系我们,并行使您的相关权利,我们会尽快回复。", + "privacyS4P2": "公司地址:浙江省杭州市滨江区长河街道江虹路410号1号楼", + "privacyS4P3": "如果您对我们的回复不满意,特别是当个人信息处理行为损害了您的合法权益时,您还可以通过向有管辖权的人民法院提起诉讼、向行业自律协会或政府相关管理机构投诉等外部途径进行解决。您也可以向我们了解可能适用的相关投诉途径的信息。", + "privacyEffective": "生效日期:2026.5.15", "updateSectionTitle": "应用更新", "updateSectionHint": "保持 BitFun 为最新版本,以获取修复与新功能。" }, diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 78b7a7444..d62f5ac64 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -486,6 +486,32 @@ "commit": "提交", "branch": "分支", "copyright": "© 2025 BitFun. All rights reserved.", + "openSource": "開源軟件", + "openSourceDesc": "BitFun 使用了以下開源組件:", + "openSourceFootnote": "點擊組件名稱可打開其官方頁面獲取更多資訊。", + "userAgreement": "隱私協議", + "openSourceFrontend": "前端", + "openSourceBackend": "後端", + "openSourceTagFE": "前端", + "openSourceTagBE": "後端", + "privacyTitle": "關於 BitFun 與隱私的聲明", + "privacyUpdated": "更新日期:2026.5.15", + "privacyIntro": "BitFun 是由 杭州華為企業通信技術有限公司 (以下簡稱\u201C我們\u201D)為您提供的,用於提供 AI 輔助程式設計與開發效率提升服務的應用。本隱私聲明由我們為處理您的個人資訊而制定。", + "privacyCommitment": "我們非常重視您的個人資訊和隱私保護,將會按照法律要求和業界成熟的安全標準,為您的個人資訊提供相應的安全保護措施。", + "privacyS1Title": "1. 我們如何收集和使用您的個人資訊", + "privacyS1P1": "我們僅在有合法性基礎的情形下才會使用您的個人資訊。根據適用的法律,我們可能會基於您的同意、為履行/訂立您與我們的合約所必需、履行法定義務所必需等合法性基礎,使用您的個人資訊。", + "privacyS1P2": "1.1 基於履行法定義務或其他法律法規規定的情形,我們可能會處理您的以下個人資訊:", + "privacyS1P3": "為了實現應用功能,在獲取您的同意後我們需要收集您的常用指令。", + "privacyS2Title": "2. 管理您的個人資訊", + "privacyS2P1": "如您對您的數據主體權利有進一步要求或存在任何疑問、意見或建議,可通過本聲明中\u201C如何聯繫我們\u201D章節中所述方式與我們取得聯繫,並行駛您的相關權利。", + "privacyS3Title": "3. 資訊存儲地點及期限", + "privacyS3P1": "3.1 我們承諾,除法律法規另有規定外,我們對您的資訊的保存期限應當為實現處理目的所必要的最短時間。", + "privacyS3P2": "3.2 上述資訊將會傳輸並保存至本地的伺服器。", + "privacyS4Title": "4. 如何聯繫我們", + "privacyS4P1": "您可通過以下方式聯繫我們,並行駛您的相關權利,我們會盡快回覆。", + "privacyS4P2": "公司地址:浙江省杭州市濱江區長河街道江虹路410號1號樓", + "privacyS4P3": "如果您對我們旳回覆不滿意,特別是當個人資訊處理行為損害了您的合法權益時,您還可以通過向有管轄權的人民法院提起訴訟、向行業自律協會或政府相關管理機構投訴等外部途徑進行解決。您也可以向我們了解可能適用的相關投訴途徑的資訊。", + "privacyEffective": "生效日期:2026.5.15", "updateSectionTitle": "應用更新", "updateSectionHint": "保持 BitFun 為最新版本,以取得修復與新功能。" }, From 971ca6123eeec67882cd26e1a0543a6a40416db6 Mon Sep 17 00:00:00 2001 From: SWangHash <88996709+SWangHash@users.noreply.github.com> Date: Sun, 17 May 2026 17:38:00 +0800 Subject: [PATCH 26/31] [fix] Cancel the image export function TicketNo: DTS2026051534905 --- .../components/modern/ExportImageButton.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx index 2fd365ff8..31aad6e89 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx @@ -2,16 +2,17 @@ * Export dialog turns as long images. * Uses React rendering to match FlowChat styles. * Uses modern-screenshot (fork of html-to-image with better CSS var / font / CORS handling). + * + * NOTE: Temporarily disabled — the export button is hidden. Keep the export + * infrastructure in place for when the underlying capture/save issues are resolved. */ import React, { useState, useCallback, useRef } from 'react'; import { createRoot } from 'react-dom/client'; -import { Image, Loader2 } from 'lucide-react'; import { FlowChatStore } from '../../store/FlowChatStore'; import { notificationService } from '@/shared/notification-system'; import { FlowTextBlock } from '../FlowTextBlock'; import { FlowToolCard } from '../FlowToolCard'; -import { Tooltip } from '@/component-library'; import type { DialogTurn, FlowTextItem, FlowToolItem, FlowThinkingItem } from '../../types/flow-chat'; import { i18nService } from '@/infrastructure/i18n'; import { workspaceAPI } from '@/infrastructure/api'; @@ -534,17 +535,13 @@ export const ExportImageButton: React.FC = ({ } }, [getDialogTurn]); - return ( - - - - ); + // Currently unused — kept for re-enable. + void (handleExport as unknown as React.MouseEventHandler); + void (isExporting as boolean); + void (isExportingRef as React.MutableRefObject); + void (className as string); + + return null; }; ExportImageButton.displayName = 'ExportImageButton'; From c4881a03d591e89f2db4dd2d94d2a0ddc248a1be Mon Sep 17 00:00:00 2001 From: SWangHash <88996709+SWangHash@users.noreply.github.com> Date: Mon, 18 May 2026 17:23:44 +0800 Subject: [PATCH 27/31] [fix] Fix breadcrumb path issue TicketNo: DTS2026051532854 --- .../editor/components/EditorBreadcrumb.tsx | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx index 37a89d8dd..6cc37a0bc 100644 --- a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx +++ b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.tsx @@ -263,35 +263,64 @@ export const EditorBreadcrumb: React.FC = ({ const parts = relativePath.split('/').filter(Boolean); if (parts.length === 0) return []; + const isUnderWorkspace = relativePath !== normalizedPath; + + log.debug('Building breadcrumb segments', { + filePath, + workspacePath, + normalizedPath, + normalizedWorkspace, + relativePath, + isUnderWorkspace, + }); + const result: PathSegment[] = []; - - // Add root directory as first level - if (normalizedWorkspace) { - const rootName = normalizedWorkspace.split('/').filter(Boolean).pop() || 'root'; - result.push({ - name: rootName, - fullPath: normalizedWorkspace, - isFile: false, - }); - } - let currentPath = normalizedWorkspace; + // File under workspace: show workspace root then relative parts + if (isUnderWorkspace) { + if (normalizedWorkspace) { + const rootName = normalizedWorkspace.split('/').filter(Boolean).pop() || 'root'; + result.push({ + name: rootName, + fullPath: normalizedWorkspace, + isFile: false, + }); + } - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - currentPath = currentPath ? `${currentPath}/${part}` : part; - result.push({ - name: part, - fullPath: currentPath, - isFile: i === parts.length - 1, - }); + let currentPath = normalizedWorkspace || ''; + for (let i = 0; i < parts.length; i++) { + currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]; + result.push({ + name: parts[i], + fullPath: currentPath, + isFile: i === parts.length - 1, + }); + } + } else { + // Absolute path outside workspace: rebuild fullPath from normalizedPath + // preserving leading root (e.g. '/' for Unix, '' for Windows drive letters) + const hasLeadingSlash = normalizedPath.startsWith('/'); + let currentPath = hasLeadingSlash ? '/' : ''; + for (let i = 0; i < parts.length; i++) { + currentPath = currentPath === '/' ? `/${parts[i]}` : currentPath ? `${currentPath}/${parts[i]}` : parts[i]; + result.push({ + name: parts[i], + fullPath: currentPath, + isFile: i === parts.length - 1, + }); + } } + log.debug('Breadcrumb segments result', { + result: result.map(s => ({ name: s.name, fullPath: s.fullPath, isFile: s.isFile })), + }); + return result; }, [filePath, workspacePath]); // Load directory contents const loadDirectoryContents = useCallback(async (dirPath: string) => { + log.debug('Loading directory contents', { dirPath }); setDropdownLoading(true); setCurrentDirPath(dirPath); try { @@ -311,9 +340,10 @@ export const EditorBreadcrumb: React.FC = ({ isDirectory: entry.isDirectory || false, })); + log.debug('Directory contents loaded', { dirPath, itemCount: items.length }); setDropdownItems(items); } catch (error) { - log.error('Failed to load directory', error); + log.error('Failed to load directory', { dirPath, error: String(error) }); setDropdownItems([]); } finally { setDropdownLoading(false); @@ -338,6 +368,13 @@ export const EditorBreadcrumb: React.FC = ({ ? segment.fullPath.substring(0, segment.fullPath.lastIndexOf('/')) : segment.fullPath; + log.debug('Breadcrumb segment clicked', { + segmentName: segment.name, + segmentFullPath: segment.fullPath, + isFile: segment.isFile, + resolvedDirPath: dirPath, + }); + setInitialDirPath(dirPath); loadDirectoryContents(dirPath); } @@ -345,6 +382,12 @@ export const EditorBreadcrumb: React.FC = ({ // Handle dropdown item selection const handleDropdownSelect = useCallback(async (item: FileItem) => { + log.debug('Breadcrumb dropdown item selected', { + name: item.name, + path: item.path, + isDirectory: item.isDirectory, + }); + if (item.isDirectory) { loadDirectoryContents(item.path); } else { From 66f257c60fcf40c22b1ea175578a043443e646ee Mon Sep 17 00:00:00 2001 From: Marqle Date: Mon, 18 May 2026 16:43:27 +0800 Subject: [PATCH 28/31] remove indent in editor && fix expand in notification --- .../components/NotificationCenter.scss | 1220 ++++++++--------- .../components/NotificationCenter.tsx | 152 +- .../tools/editor/components/CodeEditor.tsx | 30 - .../editor/components/EditorStatusBar.tsx | 44 +- 4 files changed, 702 insertions(+), 744 deletions(-) diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss index 0ee671ea0..dcd93b284 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss +++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss @@ -1,639 +1,639 @@ -@use '../../../component-library/styles/tokens' as *; + @use '../../../component-library/styles/tokens' as *; -.notification-center { - display: flex; - flex-direction: column; - height: 620px; - max-height: calc(100vh - 120px); - font-family: $font-family-sans; - overflow: hidden; + .notification-center { + display: flex; + flex-direction: column; + height: 620px; + max-height: calc(100vh - 120px); + font-family: $font-family-sans; + overflow: hidden; - &__header { - display: flex; - align-items: center; - gap: $size-gap-2; - padding: 0 $size-gap-4; - border-bottom: 1px solid var(--border-base); - background: var(--color-bg-primary); - height: 40px; - min-height: 40px; - flex-shrink: 0; - border-radius: $size-radius-base $size-radius-base 0 0; - } - - &__title { - font-size: var(--font-size-sm); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - margin: 0; - flex: 1; - } - - &__header-actions { - display: flex; - align-items: center; - gap: 2px; - flex-shrink: 0; - margin-left: auto; - } - - &__header-button { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - padding: 0; - background: transparent; - border: none; - border-radius: $size-radius-sm; - color: var(--color-text-secondary); - cursor: pointer; - transition: all $motion-base $easing-standard; - - &:hover { - background: var(--element-bg-base); - color: var(--color-text-primary); - } - - &:active { - transform: scale(0.95); - } - } + &__header { + display: flex; + align-items: center; + gap: $size-gap-2; + padding: 0 $size-gap-4; + border-bottom: 1px solid var(--border-base); + background: var(--color-bg-primary); + height: 40px; + min-height: 40px; + flex-shrink: 0; + border-radius: $size-radius-base $size-radius-base 0 0; + } + + &__title { + font-size: var(--font-size-sm); + font-weight: $font-weight-semibold; + color: var(--color-text-primary); + margin: 0; + flex: 1; + } + + &__header-actions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + margin-left: auto; + } + + &__header-button { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + background: transparent; + border: none; + border-radius: $size-radius-sm; + color: var(--color-text-secondary); + cursor: pointer; + transition: all $motion-base $easing-standard; + + &:hover { + background: var(--element-bg-base); + color: var(--color-text-primary); + } + + &:active { + transform: scale(0.95); + } + } - &__search { - margin: $size-gap-3 $size-gap-4 0; - flex-shrink: 0; - } + &__search { + margin: $size-gap-3 $size-gap-4 0; + flex-shrink: 0; + } - &__filters { - display: flex; - gap: $size-gap-1; - margin: $size-gap-3 $size-gap-4 0; - padding-bottom: $size-gap-2; - border-bottom: 1px solid var(--border-base); - flex-shrink: 0; - } - - &__filter { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 6px $size-gap-2; - background: transparent; - border: none; - border-bottom: 2px solid transparent; - font-size: var(--font-size-xs); - font-weight: $font-weight-medium; - color: var(--color-text-secondary); - cursor: pointer; - transition: all $motion-base $easing-standard; - white-space: nowrap; - border-radius: $size-radius-sm $size-radius-sm 0 0; - - &:hover { - color: var(--color-text-primary); - background: var(--element-bg-subtle); - } - - &.is-active { - color: var(--color-accent-500); - border-bottom-color: var(--color-accent-500); - background: transparent; - } - } + &__filters { + display: flex; + gap: $size-gap-1; + margin: $size-gap-3 $size-gap-4 0; + padding-bottom: $size-gap-2; + border-bottom: 1px solid var(--border-base); + flex-shrink: 0; + } + + &__filter { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 6px $size-gap-2; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + font-size: var(--font-size-xs); + font-weight: $font-weight-medium; + color: var(--color-text-secondary); + cursor: pointer; + transition: all $motion-base $easing-standard; + white-space: nowrap; + border-radius: $size-radius-sm $size-radius-sm 0 0; + + &:hover { + color: var(--color-text-primary); + background: var(--element-bg-subtle); + } + + &.is-active { + color: var(--color-accent-500); + border-bottom-color: var(--color-accent-500); + background: transparent; + } + } - &__content { - flex: 1; - overflow-y: auto; - min-height: 0; + &__content { + flex: 1; + overflow-y: auto; + min-height: 0; - } + } - &__empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px $size-gap-4; - height: 100%; - text-align: center; - color: var(--color-text-secondary); - } - - &__empty-icon { - font-size: 32px; - margin-bottom: $size-gap-3; - opacity: 0.5; - } - - &__empty-text { - font-size: var(--font-size-sm); - color: var(--color-text-muted); - } + &__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px $size-gap-4; + height: 100%; + text-align: center; + color: var(--color-text-secondary); + } + + &__empty-icon { + font-size: 32px; + margin-bottom: $size-gap-3; + opacity: 0.5; + } + + &__empty-text { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } - &__group { - margin-bottom: 2px; - - &:first-child { - margin-top: $size-gap-1; - } - } - - &__group-title { - padding: 6px $size-gap-4; - font-size: 11px; - font-weight: $font-weight-semibold; - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - background: $element-bg-subtle; - user-select: none; - } + &__group { + margin-bottom: 2px; + + &:first-child { + margin-top: $size-gap-1; + } + } + + &__group-title { + padding: 6px $size-gap-4; + font-size: 11px; + font-weight: $font-weight-semibold; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + background: $element-bg-subtle; + user-select: none; + } - &__item { - position: relative; - display: flex; - align-items: flex-start; - gap: $size-gap-3; - padding: $size-gap-3 $size-gap-4; - cursor: pointer; - transition: all $motion-base $easing-standard; - border-bottom: 1px solid transparent; - - &:hover { - background: var(--element-bg-base); - - &:not(.is-unread) { - background: var(--element-bg-subtle); - } - } - - &.is-unread { - background: rgba(96, 165, 250, 0.03); - - &:hover { - background: rgba(96, 165, 250, 0.06); - } - } - - &:active { - transform: scale(0.995); - } + &__item { + position: relative; + display: flex; + align-items: flex-start; + gap: $size-gap-3; + padding: $size-gap-3 $size-gap-4; + cursor: pointer; + transition: all $motion-base $easing-standard; + border-bottom: 1px solid transparent; + + &:hover { + background: var(--element-bg-base); + + &:not(.is-unread) { + background: var(--element-bg-subtle); + } + } + + &.is-unread { + background: rgba(96, 165, 250, 0.03); + + &:hover { + background: rgba(96, 165, 250, 0.06); + } + } + + &:active { + transform: scale(0.995); + } - &.is-expanded { - .notification-center__item-title { - white-space: normal; - overflow: visible; - text-overflow: unset; - } - - .notification-center__item-message { - -webkit-line-clamp: unset; - line-clamp: unset; - overflow: visible; - } - } - } - - &__item-icon { - flex-shrink: 0; - width: 26px; - height: 26px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - font-size: 12px; - font-weight: $font-weight-semibold; - - &--success { - background: var(--color-success-bg); - color: var(--color-success); - } - - &--error { - background: var(--color-error-bg); - color: var(--color-error); - } - - &--warning { - background: var(--color-warning-bg); - color: var(--color-warning); - } - - &--info { - background: rgba(234, 179, 8, 0.18); - color: #ca8a04; - } + &.is-expanded { + .notification-center__item-title { + white-space: normal; + overflow: visible; + text-overflow: unset; + } + + .notification-center__item-message { + -webkit-line-clamp: unset; + line-clamp: unset; + overflow: visible; + } + } + } + + &__item-icon { + flex-shrink: 0; + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 12px; + font-weight: $font-weight-semibold; + + &--success { + background: var(--color-success-bg); + color: var(--color-success); + } + + &--error { + background: var(--color-error-bg); + color: var(--color-error); + } + + &--warning { + background: var(--color-warning-bg); + color: var(--color-warning); + } + + &--info { + background: rgba(234, 179, 8, 0.18); + color: #ca8a04; + } - &--completed { - background: var(--color-success-bg); - color: var(--color-success); - } - - &--failed { - background: var(--color-error-bg); - color: var(--color-error); - } - - &--cancelled { - background: var(--element-bg-base); - color: var(--color-text-muted); - } - - &--active { - background: rgba(234, 179, 8, 0.18); - color: #ca8a04; - } - } - - &__item-content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 3px; - } - - &__item-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-1; - } - - &__item-title { - font-size: var(--font-size-sm); - font-weight: $font-weight-medium; - color: var(--color-text-primary); - line-height: 1.4; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - } - - &__item-percentage { - font-size: 11px; - font-weight: $font-weight-semibold; - color: #ca8a04; - font-variant-numeric: tabular-nums; - font-family: $font-family-mono; - flex-shrink: 0; - } - - &__item-message { - font-size: var(--font-size-xs); - color: var(--color-text-secondary); - line-height: 1.45; - margin-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - word-break: break-word; - } - - &__item-time { - font-size: 10px; - color: var(--color-text-muted); - font-family: $font-family-mono; - } - - &__item-technical-details { - margin-top: $size-gap-2; - padding: $size-gap-2; - border: 1px solid var(--border-base); - border-radius: $size-radius-sm; - background: var(--element-bg-subtle); - cursor: text; - } - - &__item-technical-title { - margin-bottom: 4px; - font-size: 10px; - font-weight: $font-weight-semibold; - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - } - - &__item-technical-body { - margin: 0; - max-height: 160px; - overflow: auto; - white-space: pre-wrap; - word-break: break-word; - font-family: $font-family-mono; - font-size: 11px; - line-height: 1.45; - color: var(--color-text-secondary); - } - - - &__item-progress-bar { - width: 100%; - height: 3px; - background: $element-bg-base; - border-radius: 1px; - overflow: hidden; - margin: 4px 0; - } - - &__item-progress-fill { - height: 100%; - background: linear-gradient(90deg, $color-accent-600, $color-purple-500); - border-radius: 2px; - transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); - - &.is-completed { - background: var(--color-success); - } - - &.is-failed { - background: var(--color-error); - } - - &.is-cancelled { - background: $color-text-muted; - } - } - - &__item-badge { - flex-shrink: 0; - width: 10px; - height: 10px; - background: var(--color-error); - border-radius: 50%; - border: none; - margin-left: auto; - margin-right: $size-gap-1; - align-self: center; - } - - &__item-actions { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 4px; - margin-left: $size-gap-2; - } - - &__item-expand { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - background: transparent; - border: none; - border-radius: $size-radius-sm; - color: var(--color-text-muted); - cursor: pointer; - opacity: 0; - transition: all $motion-base $easing-standard; - - &:hover { - background: var(--element-bg-base); - color: var(--color-text-primary); - opacity: 1 !important; - } - - &:active { - transform: scale(0.9); - } - } - - &__item-delete { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - background: transparent; - border: none; - border-radius: $size-radius-sm; - color: var(--color-text-muted); - cursor: pointer; - opacity: 0; - transition: all $motion-base $easing-standard; - - &:hover { - background: var(--element-bg-base); - color: var(--color-error); - opacity: 1 !important; - } - - &:active { - transform: scale(0.9); - } - } - - &__item:hover &__item-expand, - &__item:hover &__item-delete { - opacity: 1; - } + &--completed { + background: var(--color-success-bg); + color: var(--color-success); + } + + &--failed { + background: var(--color-error-bg); + color: var(--color-error); + } + + &--cancelled { + background: var(--element-bg-base); + color: var(--color-text-muted); + } + + &--active { + background: rgba(234, 179, 8, 0.18); + color: #ca8a04; + } + } + + &__item-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; + } + + &__item-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: $size-gap-1; + } + + &__item-title { + font-size: var(--font-size-sm); + font-weight: $font-weight-medium; + color: var(--color-text-primary); + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + } + + &__item-percentage { + font-size: 11px; + font-weight: $font-weight-semibold; + color: #ca8a04; + font-variant-numeric: tabular-nums; + font-family: $font-family-mono; + flex-shrink: 0; + } + + &__item-message { + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + line-height: 1.45; + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + word-break: break-word; + } + + &__item-time { + font-size: 10px; + color: var(--color-text-muted); + font-family: $font-family-mono; + } + + &__item-technical-details { + margin-top: $size-gap-2; + padding: $size-gap-2; + border: 1px solid var(--border-base); + border-radius: $size-radius-sm; + background: var(--element-bg-subtle); + cursor: text; + } + + &__item-technical-title { + margin-bottom: 4px; + font-size: 10px; + font-weight: $font-weight-semibold; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__item-technical-body { + margin: 0; + max-height: 160px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + font-family: $font-family-mono; + font-size: 11px; + line-height: 1.45; + color: var(--color-text-secondary); + } - &__item.is-expanded &__item-expand { - opacity: 1; - color: var(--color-text-primary); - } + &__item-progress-bar { + width: 100%; + height: 3px; + background: $element-bg-base; + border-radius: 1px; + overflow: hidden; + margin: 4px 0; + } + + &__item-progress-fill { + height: 100%; + background: linear-gradient(90deg, $color-accent-600, $color-purple-500); + border-radius: 2px; + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &.is-completed { + background: var(--color-success); + } + + &.is-failed { + background: var(--color-error); + } + + &.is-cancelled { + background: $color-text-muted; + } + } + + &__item-badge { + flex-shrink: 0; + width: 10px; + height: 10px; + background: var(--color-error); + border-radius: 50%; + border: none; + position: absolute; + right: 80px; + align-self: center; + } + + &__item-actions { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 4px; + margin-left: auto; + } + + &__item-expand { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + background: transparent; + border: none; + border-radius: $size-radius-sm; + color: var(--color-text-muted); + cursor: pointer; + opacity: 0; + transition: all $motion-base $easing-standard; + + &:hover { + background: var(--element-bg-base); + color: var(--color-text-primary); + opacity: 1 !important; + } + + &:active { + transform: scale(0.9); + } + } + + &__item-delete { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + background: transparent; + border: none; + border-radius: $size-radius-sm; + color: var(--color-text-muted); + cursor: pointer; + opacity: 0; + transition: all $motion-base $easing-standard; + + &:hover { + background: var(--element-bg-base); + color: var(--color-error); + opacity: 1 !important; + } + + &:active { + transform: scale(0.9); + } + } + + &__item:hover &__item-expand, + &__item:hover &__item-delete { + opacity: 1; + } + + + &__item.is-expanded &__item-expand { + opacity: 1; + color: var(--color-text-primary); + } - &__active-section { - padding: $size-gap-3 0; - border-bottom: 1px solid var(--border-base); - background: rgba(59, 130, 246, 0.02); - } - - &__active-section-title { - padding: 6px $size-gap-4; - font-size: 11px; - font-weight: $font-weight-semibold; - color: var(--color-accent-500); - text-transform: uppercase; - letter-spacing: 0.5px; - user-select: none; - } - - &__active-section-list { - display: flex; - flex-direction: column; - gap: 2px; - } - - - &__active-task-item { - display: flex; - align-items: flex-start; - gap: $size-gap-3; - padding: $size-gap-3 $size-gap-4; - transition: background $motion-base $easing-standard; - - &:hover { - background: rgba(59, 130, 246, 0.05); - } - } - - &__active-task-icon { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - color: var(--color-accent-500); - margin-top: 1px; - } - - &__spinner { - animation: bitfun-notification-center-spin 1s linear infinite; - } - - &__active-task-content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 3px; - } - - &__active-task-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $size-gap-1; - } - - &__active-task-title { - font-size: var(--font-size-sm); - font-weight: $font-weight-medium; - color: var(--color-text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - } - - &__active-task-progress-text { - font-size: 11px; - font-weight: $font-weight-semibold; - color: var(--color-accent-500); - font-variant-numeric: tabular-nums; - font-family: $font-family-mono; - flex-shrink: 0; - } - - &__active-task-message { - font-size: var(--font-size-xs); - color: var(--color-text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__active-task-progress-bar { - width: 100%; - height: 3px; - background: $element-bg-base; - border-radius: 1px; - overflow: hidden; - margin-top: 4px; - } - - &__active-task-progress-fill { - height: 100%; - background: linear-gradient(90deg, $color-accent-600, $color-purple-500); - border-radius: 1px; - transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); - } - - - &__active-loading-item { - display: flex; - align-items: center; - gap: $size-gap-3; - padding: $size-gap-3 $size-gap-4; - transition: background $motion-base $easing-standard; - - &:hover { - background: rgba(59, 130, 246, 0.05); - } - } - - &__active-loading-icon { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - color: var(--color-accent-500); - } - - &__active-loading-content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 3px; - } - - &__active-loading-title { - font-size: var(--font-size-sm); - font-weight: $font-weight-medium; - color: var(--color-text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__active-loading-message { - font-size: var(--font-size-xs); - color: var(--color-text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} - - -@keyframes bitfun-notification-center-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - - -@media (max-width: 768px) { - .notification-center { - height: 78vh; - max-height: calc(100vh - 80px); - } -} + &__active-section { + padding: $size-gap-3 0; + border-bottom: 1px solid var(--border-base); + background: rgba(59, 130, 246, 0.02); + } + + &__active-section-title { + padding: 6px $size-gap-4; + font-size: 11px; + font-weight: $font-weight-semibold; + color: var(--color-accent-500); + text-transform: uppercase; + letter-spacing: 0.5px; + user-select: none; + } + + &__active-section-list { + display: flex; + flex-direction: column; + gap: 2px; + } + + + &__active-task-item { + display: flex; + align-items: flex-start; + gap: $size-gap-3; + padding: $size-gap-3 $size-gap-4; + transition: background $motion-base $easing-standard; + + &:hover { + background: rgba(59, 130, 246, 0.05); + } + } + + &__active-task-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: var(--color-accent-500); + margin-top: 1px; + } + + &__spinner { + animation: bitfun-notification-center-spin 1s linear infinite; + } + + &__active-task-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; + } + + &__active-task-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: $size-gap-1; + } + + &__active-task-title { + font-size: var(--font-size-sm); + font-weight: $font-weight-medium; + color: var(--color-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + } + + &__active-task-progress-text { + font-size: 11px; + font-weight: $font-weight-semibold; + color: var(--color-accent-500); + font-variant-numeric: tabular-nums; + font-family: $font-family-mono; + flex-shrink: 0; + } + + &__active-task-message { + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__active-task-progress-bar { + width: 100%; + height: 3px; + background: $element-bg-base; + border-radius: 1px; + overflow: hidden; + margin-top: 4px; + } + + &__active-task-progress-fill { + height: 100%; + background: linear-gradient(90deg, $color-accent-600, $color-purple-500); + border-radius: 1px; + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + + &__active-loading-item { + display: flex; + align-items: center; + gap: $size-gap-3; + padding: $size-gap-3 $size-gap-4; + transition: background $motion-base $easing-standard; + + &:hover { + background: rgba(59, 130, 246, 0.05); + } + } + + &__active-loading-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: var(--color-accent-500); + } + + &__active-loading-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; + } + + &__active-loading-title { + font-size: var(--font-size-sm); + font-weight: $font-weight-medium; + color: var(--color-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__active-loading-message { + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + + @keyframes bitfun-notification-center-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + + @media (max-width: 768px) { + .notification-center { + height: 78vh; + max-height: calc(100vh - 80px); + } + } diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx index 147614d0f..61f12c77f 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx +++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx @@ -1,4 +1,4 @@ - + import React, { useState, useMemo } from 'react'; import { X, CheckCheck, Trash2, XCircle, ChevronDown, ChevronUp, Loader2 } from 'lucide-react'; @@ -21,30 +21,29 @@ export const NotificationCenter: React.FC = () => { return [...allProgressNotifications, ...allLoadingNotifications]; }, [allProgressNotifications, allLoadingNotifications]); - const handleClose = React.useCallback(() => { notificationService.toggleCenter(false); }, []); - + const handleMarkAllRead = () => { notificationService.markAllAsRead(); }; - + const handleClearAll = () => { notificationService.clearHistory(); }; - + const handleDeleteNotification = (e: React.MouseEvent, notificationId: string) => { - e.stopPropagation(); + e.stopPropagation(); notificationService.deleteFromHistory(notificationId); }; - + const handleNotificationClick = (notification: NotificationRecord) => { - + setExpandedIds(prev => { const newSet = new Set(prev); if (newSet.has(notification.id)) { @@ -55,37 +54,37 @@ export const NotificationCenter: React.FC = () => { return newSet; }); - + if (!notification.read) { notificationService.markAsRead(notification.id); } - - + + if (notification.metadata?.onClick) { notification.metadata.onClick(); } }; - + const filteredHistory = useMemo(() => { let filtered = history; - - + + filtered = filtered.filter(n => { if (n.variant === 'progress' || n.variant === 'loading') { - + return n.status === 'completed' || n.status === 'failed' || n.status === 'cancelled'; } - return true; + return true; }); - + if (filter !== 'all') { filtered = filtered.filter(n => n.type === filter); } - + if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(n => @@ -97,7 +96,7 @@ export const NotificationCenter: React.FC = () => { return filtered; }, [history, filter, searchQuery]); - + const groupedHistory = useMemo(() => { const now = Date.now(); const today = new Date(now).setHours(0, 0, 0, 0); @@ -111,7 +110,7 @@ export const NotificationCenter: React.FC = () => { filteredHistory.forEach(notification => { const notificationDate = new Date(notification.timestamp).setHours(0, 0, 0, 0); - + if (notificationDate === today) { groups.today.push(notification); } else if (notificationDate === yesterday) { @@ -124,14 +123,14 @@ export const NotificationCenter: React.FC = () => { return groups; }, [filteredHistory]); - + const formatTime = (timestamp: number) => { return formatDate(timestamp, { hour: '2-digit', minute: '2-digit' }); }; - + const getIcon = (type: string, status?: string) => { - + if (status === 'completed') { return '✓'; } @@ -141,8 +140,8 @@ export const NotificationCenter: React.FC = () => { if (status === 'cancelled') { return '⊘'; } - - + + switch (type) { case 'success': return '✓'; @@ -169,35 +168,35 @@ export const NotificationCenter: React.FC = () => { return diagnostics || (rawError ? `raw_error=${rawError}` : null); }; - + const renderActiveTaskItem = (notification: Notification) => { const isProgress = notification.variant === 'progress'; const isLoading = notification.variant === 'loading'; - - + + const getProgressInfo = () => { if (isLoading) { - return null; + return null; } - + if (isProgress) { const mode = notification.progressMode || (notification.textOnly ? 'text-only' : 'percentage'); if (mode === 'text-only') return null; - + if (mode === 'fraction' && notification.current !== undefined && notification.total !== undefined) { return `${notification.current}/${notification.total}`; } - + if (mode === 'percentage' && notification.progress !== undefined) { return `${Math.round(notification.progress)}%`; } } - + return null; }; - + const progressInfo = getProgressInfo(); - + return (
{
{isProgress && notification.progressText ? notification.progressText : (notification.messageNode ?? notification.message)}
- + {isProgress && (() => { const mode = notification.progressMode || (notification.textOnly ? 'text-only' : 'percentage'); if (mode === 'text-only') return null; - + return (
{ ); }; - + const renderNotificationItem = (notification: NotificationRecord) => { const isProgress = notification.variant === 'progress'; const isLoading = notification.variant === 'loading'; - const iconClass = (isProgress || isLoading) && notification.status - ? `notification-center__item-icon--${notification.status}` + const iconClass = (isProgress || isLoading) && notification.status + ? `notification-center__item-icon--${notification.status}` : `notification-center__item-icon--${notification.type}`; - + const now = Date.now(); const today = new Date(now).setHours(0, 0, 0, 0); const yesterday = today - 86400000; const notificationDate = new Date(notification.timestamp).setHours(0, 0, 0, 0); - + const timeDisplay = notificationDate < yesterday ? formatDate(notification.timestamp, { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) : formatTime(notification.timestamp); const isExpanded = expandedIds.has(notification.id); @@ -278,30 +277,30 @@ export const NotificationCenter: React.FC = () => {
{notification.title}
- + {isProgress && (() => { const mode = notification.progressMode || (notification.textOnly ? 'text-only' : 'percentage'); if (mode === 'text-only') return null; - + if (mode === 'fraction' && notification.current !== undefined && notification.total !== undefined) { return
{notification.current}/{notification.total}
; } - + if (mode === 'percentage' && notification.progress !== undefined) { return
{Math.round(notification.progress)}%
; } - + return null; })()}
{(isProgress && notification.progressText) ? notification.progressText : (notification.messageNode ?? notification.message)}
- + {isProgress && (() => { const mode = notification.progressMode || (notification.textOnly ? 'text-only' : 'percentage'); if (mode === 'text-only') return null; - + return (
{
{!notification.read &&
}
- + {technicalDetails && ( + + )}
- +
- + {activeTaskNotifications.length > 0 && (
@@ -448,7 +458,7 @@ export const NotificationCenter: React.FC = () => {
) : ( <> - + {groupedHistory.today.length > 0 && (
{t('common:time.today')}
@@ -456,7 +466,7 @@ export const NotificationCenter: React.FC = () => {
)} - + {groupedHistory.yesterday.length > 0 && (
{t('common:time.yesterday')}
@@ -464,7 +474,7 @@ export const NotificationCenter: React.FC = () => {
)} - + {groupedHistory.earlier.length > 0 && (
{t('components:notificationCenter.groups.earlier')}
diff --git a/src/web-ui/src/tools/editor/components/CodeEditor.tsx b/src/web-ui/src/tools/editor/components/CodeEditor.tsx index aa1972989..e6cccdd0d 100644 --- a/src/web-ui/src/tools/editor/components/CodeEditor.tsx +++ b/src/web-ui/src/tools/editor/components/CodeEditor.tsx @@ -46,7 +46,6 @@ import { EditorStatusBar } from './EditorStatusBar'; const log = createLogger('CodeEditor'); import { GoToLinePopover, - IndentPopover, EncodingPopover, LanguagePopover, } from './StatusBarPopovers'; @@ -1258,23 +1257,6 @@ const CodeEditor: React.FC = ({ if (editor && model) performJump(editor, model, line, column); }, [performJump]); - const handleIndentConfirm = useCallback((tabSize: number, insertSpaces: boolean) => { - const merged = { tab_size: tabSize, insert_spaces: insertSpaces }; - userIndentRef.current = merged; - setEditorConfig((prev) => ({ ...prev, ...merged })); - const editor = editorRef.current; - if (editor) { - editor.updateOptions({ tabSize, insertSpaces }); - } - // Async persistence, don't block UI update, don't trigger applyConfig override - configManager.getConfig('editor').then((config) => { - const fullMerged = { ...(config || {}), ...merged }; - return configManager.setConfig('editor', fullMerged); - }).catch((err) => { - log.warn('Failed to persist indent config', err); - }); - }, []); - const fetchFileMetadata = useCallback(async () => { const { workspaceAPI } = await import('@/infrastructure/api'); return workspaceAPI.getFileMetadata(filePath); @@ -2154,8 +2136,6 @@ const CodeEditor: React.FC = ({ selectedLines={selection.lines} language={detectedLanguage} encoding={encoding} - tabSize={editorConfig.tab_size || 2} - insertSpaces={editorConfig.insert_spaces !== false} isReadOnly={readOnly} lspStatus={ enableLsp && lspExtensionRegistry.isFileSupported(filePath) @@ -2163,7 +2143,6 @@ const CodeEditor: React.FC = ({ : undefined } onPositionClick={(e) => openStatusBarPopover('position', e)} - onIndentClick={(e) => openStatusBarPopover('indent', e)} onEncodingClick={(e) => openStatusBarPopover('encoding', e)} onLanguageClick={(e) => openStatusBarPopover('language', e)} /> @@ -2177,15 +2156,6 @@ const CodeEditor: React.FC = ({ onClose={closeStatusBarPopover} /> )} - {statusBarPopover === 'indent' && statusBarAnchorRect && ( - - )} {statusBarPopover === 'encoding' && statusBarAnchorRect && ( void; /** Encoding click callback */ onEncodingClick?: (e: React.MouseEvent) => void; - /** Indent click callback */ - onIndentClick?: (e: React.MouseEvent) => void; /** Position click callback */ onPositionClick?: (e: React.MouseEvent) => void; } @@ -96,21 +90,21 @@ const getLspStatusInfo = ( ) => { switch (status) { case 'connected': - return { - icon: , + return { + icon: , className: 'editor-status-bar__lsp--connected', title: t('editor.statusBar.lspConnected') }; case 'connecting': - return { - icon: , + return { + icon: , className: 'editor-status-bar__lsp--connecting', title: t('editor.statusBar.lspConnecting') }; case 'disconnected': default: - return { - icon: , + return { + icon: , className: 'editor-status-bar__lsp--disconnected', title: t('editor.statusBar.lspDisconnected') }; @@ -124,13 +118,10 @@ export const EditorStatusBar: React.FC = ({ selectedLines = 0, language, encoding = 'UTF-8', - tabSize = 2, - insertSpaces = true, isReadOnly = false, lspStatus, onLanguageClick, onEncodingClick, - onIndentClick, onPositionClick, }) => { const { t } = useI18n('tools'); @@ -159,7 +150,7 @@ export const EditorStatusBar: React.FC = ({
-
@@ -170,21 +161,8 @@ export const EditorStatusBar: React.FC = ({
-
- - -
- {insertSpaces ? t('editor.statusBar.indentSpaces', { n: tabSize }) : t('editor.statusBar.indentTab', { n: tabSize })} -
-
- -
- -
@@ -195,7 +173,7 @@ export const EditorStatusBar: React.FC = ({
-
@@ -206,7 +184,7 @@ export const EditorStatusBar: React.FC = ({ {lspStatus && ( <>
-
From 2bfec884e2196340f4ad7021507f019b07458018 Mon Sep 17 00:00:00 2001 From: Marqle Date: Tue, 19 May 2026 09:43:49 +0800 Subject: [PATCH 29/31] fix esc --- src/web-ui/src/tools/editor/components/CodeEditor.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/web-ui/src/tools/editor/components/CodeEditor.tsx b/src/web-ui/src/tools/editor/components/CodeEditor.tsx index e6cccdd0d..4070ced3e 100644 --- a/src/web-ui/src/tools/editor/components/CodeEditor.tsx +++ b/src/web-ui/src/tools/editor/components/CodeEditor.tsx @@ -412,6 +412,16 @@ const CodeEditor: React.FC = ({ return () => document.removeEventListener('mousedown', onMouseDown, true); }, [statusBarPopover]); + useEffect(() => { + if (!statusBarPopover) return; + const handleClosePreview = () => { + setStatusBarPopover(null); + setStatusBarAnchorRect(null); + }; + window.addEventListener('closePreview', handleClosePreview); + return () => window.removeEventListener('closePreview', handleClosePreview); + }, [statusBarPopover]); + // Sync font/config to editor when editorConfig changes (fixes late getConfig when opening from file tree) useEffect(() => { if (!monacoReady || !editorRef.current) return; From 6bdab44488bb5656e4c1a12d1e9f4ed3ed68f0a7 Mon Sep 17 00:00:00 2001 From: Marqle Date: Tue, 19 May 2026 15:31:23 +0800 Subject: [PATCH 30/31] fix click expanded --- src/apps/{vcoder => ohos}/.gitignore | 0 src/apps/{vcoder => ohos}/AppScope/app.json5 | 0 .../resources/base/element/string.json | 0 .../resources/base/media/background.png | Bin .../resources/base/media/foreground.png | Bin .../resources/base/media/layered_image.json | 0 src/apps/{vcoder => ohos}/build-profile.json5 | 0 src/apps/{vcoder => ohos}/code-linter.json5 | 0 src/apps/{vcoder => ohos}/entry/.gitignore | 0 .../entry/build-profile.json5 | 0 src/apps/{vcoder => ohos}/entry/hvigorfile.ts | 0 .../entry/obfuscation-rules.txt | 0 .../entry/oh-package-lock.json5 | 0 .../{vcoder => ohos}/entry/oh-package.json5 | 0 .../entry/src/main/cpp/CMakeLists.txt | 0 .../entry/src/main/cpp/napi_init.cpp | 0 .../types/libbitfun_desktop_lib/Index.d.ts | 0 .../libbitfun_desktop_lib/oh-package.json5 | 0 .../src/main/cpp/types/libentry/Index.d.ts | 0 .../main/cpp/types/libentry/oh-package.json5 | 0 .../main/ets/entryability/EntryAbility.ets | 0 .../entrybackupability/EntryBackupAbility.ets | 0 .../entry/src/main/ets/pages/Index.ets | 0 .../main/ets/utils/CommonEventListener.ets | 0 .../entry/src/main/ets/utils/CommonUtils.ets | 0 .../entry/src/main/ets/utils/DevecoStart.ets | 0 .../entry/src/main/ets/utils/Result.ets | 0 .../entry/src/main/module.json5 | 0 .../main/resources/base/element/color.json | 0 .../main/resources/base/element/float.json | 0 .../main/resources/base/element/string.json | 0 .../main/resources/base/media/background.png | Bin .../main/resources/base/media/bitfun_icon.png | Bin .../main/resources/base/media/foreground.png | Bin .../resources/base/media/layered_image.json | 0 .../main/resources/base/media/startIcon.png | Bin .../resources/base/profile/backup_config.json | 0 .../resources/base/profile/main_pages.json | 0 .../main/resources/dark/element/color.json | 0 .../entry/src/mock/mock-config.json5 | 0 .../src/ohosTest/ets/test/Ability.test.ets | 0 .../entry/src/ohosTest/ets/test/List.test.ets | 0 .../entry/src/ohosTest/module.json5 | 0 .../entry/src/test/List.test.ets | 0 .../entry/src/test/LocalUnit.test.ets | 0 .../hvigor/hvigor-config.json5 | 0 src/apps/{vcoder => ohos}/hvigorfile.ts | 0 .../{vcoder => ohos}/oh-package-lock.json5 | 0 src/apps/{vcoder => ohos}/oh-package.json5 | 0 .../flow_chat/tool-cards/CompactToolCard.tsx | 17 ++++++++---- .../flow_chat/tool-cards/DefaultToolCard.tsx | 22 ++++++++++++--- .../flow_chat/tool-cards/GitToolDisplay.tsx | 19 ++++++++++--- .../tool-cards/GlobSearchDisplay.tsx | 18 ++++++++++--- .../tool-cards/GrepSearchDisplay.tsx | 19 ++++++++++--- .../src/flow_chat/tool-cards/LSDisplay.tsx | 16 ++++++++--- .../tool-cards/ModelThinkingDisplay.tsx | 23 +++++++++++++--- .../flow_chat/tool-cards/ReadFileDisplay.tsx | 23 ++++++++++++++-- .../tool-cards/SessionControlToolCard.tsx | 25 ++++++++++++++---- .../tool-cards/SessionMessageToolCard.tsx | 25 ++++++++++++++---- .../flow_chat/tool-cards/TerminalToolCard.tsx | 22 ++++++++++++--- .../flow_chat/tool-cards/WebSearchCard.tsx | 19 ++++++++++--- 61 files changed, 204 insertions(+), 44 deletions(-) rename src/apps/{vcoder => ohos}/.gitignore (100%) rename src/apps/{vcoder => ohos}/AppScope/app.json5 (100%) rename src/apps/{vcoder => ohos}/AppScope/resources/base/element/string.json (100%) rename src/apps/{vcoder => ohos}/AppScope/resources/base/media/background.png (100%) rename src/apps/{vcoder => ohos}/AppScope/resources/base/media/foreground.png (100%) rename src/apps/{vcoder => ohos}/AppScope/resources/base/media/layered_image.json (100%) rename src/apps/{vcoder => ohos}/build-profile.json5 (100%) rename src/apps/{vcoder => ohos}/code-linter.json5 (100%) rename src/apps/{vcoder => ohos}/entry/.gitignore (100%) rename src/apps/{vcoder => ohos}/entry/build-profile.json5 (100%) rename src/apps/{vcoder => ohos}/entry/hvigorfile.ts (100%) rename src/apps/{vcoder => ohos}/entry/obfuscation-rules.txt (100%) rename src/apps/{vcoder => ohos}/entry/oh-package-lock.json5 (100%) rename src/apps/{vcoder => ohos}/entry/oh-package.json5 (100%) rename src/apps/{vcoder => ohos}/entry/src/main/cpp/CMakeLists.txt (100%) rename src/apps/{vcoder => ohos}/entry/src/main/cpp/napi_init.cpp (100%) rename src/apps/{vcoder => ohos}/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts (100%) rename src/apps/{vcoder => ohos}/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 (100%) rename src/apps/{vcoder => ohos}/entry/src/main/cpp/types/libentry/Index.d.ts (100%) rename src/apps/{vcoder => ohos}/entry/src/main/cpp/types/libentry/oh-package.json5 (100%) rename src/apps/{vcoder => ohos}/entry/src/main/ets/entryability/EntryAbility.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/main/ets/pages/Index.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/main/ets/utils/CommonEventListener.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/main/ets/utils/CommonUtils.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/main/ets/utils/DevecoStart.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/main/ets/utils/Result.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/main/module.json5 (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/element/color.json (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/element/float.json (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/element/string.json (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/media/background.png (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/media/bitfun_icon.png (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/media/foreground.png (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/media/layered_image.json (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/media/startIcon.png (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/profile/backup_config.json (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/base/profile/main_pages.json (100%) rename src/apps/{vcoder => ohos}/entry/src/main/resources/dark/element/color.json (100%) rename src/apps/{vcoder => ohos}/entry/src/mock/mock-config.json5 (100%) rename src/apps/{vcoder => ohos}/entry/src/ohosTest/ets/test/Ability.test.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/ohosTest/ets/test/List.test.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/ohosTest/module.json5 (100%) rename src/apps/{vcoder => ohos}/entry/src/test/List.test.ets (100%) rename src/apps/{vcoder => ohos}/entry/src/test/LocalUnit.test.ets (100%) rename src/apps/{vcoder => ohos}/hvigor/hvigor-config.json5 (100%) rename src/apps/{vcoder => ohos}/hvigorfile.ts (100%) rename src/apps/{vcoder => ohos}/oh-package-lock.json5 (100%) rename src/apps/{vcoder => ohos}/oh-package.json5 (100%) diff --git a/src/apps/vcoder/.gitignore b/src/apps/ohos/.gitignore similarity index 100% rename from src/apps/vcoder/.gitignore rename to src/apps/ohos/.gitignore diff --git a/src/apps/vcoder/AppScope/app.json5 b/src/apps/ohos/AppScope/app.json5 similarity index 100% rename from src/apps/vcoder/AppScope/app.json5 rename to src/apps/ohos/AppScope/app.json5 diff --git a/src/apps/vcoder/AppScope/resources/base/element/string.json b/src/apps/ohos/AppScope/resources/base/element/string.json similarity index 100% rename from src/apps/vcoder/AppScope/resources/base/element/string.json rename to src/apps/ohos/AppScope/resources/base/element/string.json diff --git a/src/apps/vcoder/AppScope/resources/base/media/background.png b/src/apps/ohos/AppScope/resources/base/media/background.png similarity index 100% rename from src/apps/vcoder/AppScope/resources/base/media/background.png rename to src/apps/ohos/AppScope/resources/base/media/background.png diff --git a/src/apps/vcoder/AppScope/resources/base/media/foreground.png b/src/apps/ohos/AppScope/resources/base/media/foreground.png similarity index 100% rename from src/apps/vcoder/AppScope/resources/base/media/foreground.png rename to src/apps/ohos/AppScope/resources/base/media/foreground.png diff --git a/src/apps/vcoder/AppScope/resources/base/media/layered_image.json b/src/apps/ohos/AppScope/resources/base/media/layered_image.json similarity index 100% rename from src/apps/vcoder/AppScope/resources/base/media/layered_image.json rename to src/apps/ohos/AppScope/resources/base/media/layered_image.json diff --git a/src/apps/vcoder/build-profile.json5 b/src/apps/ohos/build-profile.json5 similarity index 100% rename from src/apps/vcoder/build-profile.json5 rename to src/apps/ohos/build-profile.json5 diff --git a/src/apps/vcoder/code-linter.json5 b/src/apps/ohos/code-linter.json5 similarity index 100% rename from src/apps/vcoder/code-linter.json5 rename to src/apps/ohos/code-linter.json5 diff --git a/src/apps/vcoder/entry/.gitignore b/src/apps/ohos/entry/.gitignore similarity index 100% rename from src/apps/vcoder/entry/.gitignore rename to src/apps/ohos/entry/.gitignore diff --git a/src/apps/vcoder/entry/build-profile.json5 b/src/apps/ohos/entry/build-profile.json5 similarity index 100% rename from src/apps/vcoder/entry/build-profile.json5 rename to src/apps/ohos/entry/build-profile.json5 diff --git a/src/apps/vcoder/entry/hvigorfile.ts b/src/apps/ohos/entry/hvigorfile.ts similarity index 100% rename from src/apps/vcoder/entry/hvigorfile.ts rename to src/apps/ohos/entry/hvigorfile.ts diff --git a/src/apps/vcoder/entry/obfuscation-rules.txt b/src/apps/ohos/entry/obfuscation-rules.txt similarity index 100% rename from src/apps/vcoder/entry/obfuscation-rules.txt rename to src/apps/ohos/entry/obfuscation-rules.txt diff --git a/src/apps/vcoder/entry/oh-package-lock.json5 b/src/apps/ohos/entry/oh-package-lock.json5 similarity index 100% rename from src/apps/vcoder/entry/oh-package-lock.json5 rename to src/apps/ohos/entry/oh-package-lock.json5 diff --git a/src/apps/vcoder/entry/oh-package.json5 b/src/apps/ohos/entry/oh-package.json5 similarity index 100% rename from src/apps/vcoder/entry/oh-package.json5 rename to src/apps/ohos/entry/oh-package.json5 diff --git a/src/apps/vcoder/entry/src/main/cpp/CMakeLists.txt b/src/apps/ohos/entry/src/main/cpp/CMakeLists.txt similarity index 100% rename from src/apps/vcoder/entry/src/main/cpp/CMakeLists.txt rename to src/apps/ohos/entry/src/main/cpp/CMakeLists.txt diff --git a/src/apps/vcoder/entry/src/main/cpp/napi_init.cpp b/src/apps/ohos/entry/src/main/cpp/napi_init.cpp similarity index 100% rename from src/apps/vcoder/entry/src/main/cpp/napi_init.cpp rename to src/apps/ohos/entry/src/main/cpp/napi_init.cpp diff --git a/src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts b/src/apps/ohos/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts similarity index 100% rename from src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts rename to src/apps/ohos/entry/src/main/cpp/types/libbitfun_desktop_lib/Index.d.ts diff --git a/src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 b/src/apps/ohos/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 similarity index 100% rename from src/apps/vcoder/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 rename to src/apps/ohos/entry/src/main/cpp/types/libbitfun_desktop_lib/oh-package.json5 diff --git a/src/apps/vcoder/entry/src/main/cpp/types/libentry/Index.d.ts b/src/apps/ohos/entry/src/main/cpp/types/libentry/Index.d.ts similarity index 100% rename from src/apps/vcoder/entry/src/main/cpp/types/libentry/Index.d.ts rename to src/apps/ohos/entry/src/main/cpp/types/libentry/Index.d.ts diff --git a/src/apps/vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 b/src/apps/ohos/entry/src/main/cpp/types/libentry/oh-package.json5 similarity index 100% rename from src/apps/vcoder/entry/src/main/cpp/types/libentry/oh-package.json5 rename to src/apps/ohos/entry/src/main/cpp/types/libentry/oh-package.json5 diff --git a/src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets b/src/apps/ohos/entry/src/main/ets/entryability/EntryAbility.ets similarity index 100% rename from src/apps/vcoder/entry/src/main/ets/entryability/EntryAbility.ets rename to src/apps/ohos/entry/src/main/ets/entryability/EntryAbility.ets diff --git a/src/apps/vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/src/apps/ohos/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets similarity index 100% rename from src/apps/vcoder/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets rename to src/apps/ohos/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets diff --git a/src/apps/vcoder/entry/src/main/ets/pages/Index.ets b/src/apps/ohos/entry/src/main/ets/pages/Index.ets similarity index 100% rename from src/apps/vcoder/entry/src/main/ets/pages/Index.ets rename to src/apps/ohos/entry/src/main/ets/pages/Index.ets diff --git a/src/apps/vcoder/entry/src/main/ets/utils/CommonEventListener.ets b/src/apps/ohos/entry/src/main/ets/utils/CommonEventListener.ets similarity index 100% rename from src/apps/vcoder/entry/src/main/ets/utils/CommonEventListener.ets rename to src/apps/ohos/entry/src/main/ets/utils/CommonEventListener.ets diff --git a/src/apps/vcoder/entry/src/main/ets/utils/CommonUtils.ets b/src/apps/ohos/entry/src/main/ets/utils/CommonUtils.ets similarity index 100% rename from src/apps/vcoder/entry/src/main/ets/utils/CommonUtils.ets rename to src/apps/ohos/entry/src/main/ets/utils/CommonUtils.ets diff --git a/src/apps/vcoder/entry/src/main/ets/utils/DevecoStart.ets b/src/apps/ohos/entry/src/main/ets/utils/DevecoStart.ets similarity index 100% rename from src/apps/vcoder/entry/src/main/ets/utils/DevecoStart.ets rename to src/apps/ohos/entry/src/main/ets/utils/DevecoStart.ets diff --git a/src/apps/vcoder/entry/src/main/ets/utils/Result.ets b/src/apps/ohos/entry/src/main/ets/utils/Result.ets similarity index 100% rename from src/apps/vcoder/entry/src/main/ets/utils/Result.ets rename to src/apps/ohos/entry/src/main/ets/utils/Result.ets diff --git a/src/apps/vcoder/entry/src/main/module.json5 b/src/apps/ohos/entry/src/main/module.json5 similarity index 100% rename from src/apps/vcoder/entry/src/main/module.json5 rename to src/apps/ohos/entry/src/main/module.json5 diff --git a/src/apps/vcoder/entry/src/main/resources/base/element/color.json b/src/apps/ohos/entry/src/main/resources/base/element/color.json similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/element/color.json rename to src/apps/ohos/entry/src/main/resources/base/element/color.json diff --git a/src/apps/vcoder/entry/src/main/resources/base/element/float.json b/src/apps/ohos/entry/src/main/resources/base/element/float.json similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/element/float.json rename to src/apps/ohos/entry/src/main/resources/base/element/float.json diff --git a/src/apps/vcoder/entry/src/main/resources/base/element/string.json b/src/apps/ohos/entry/src/main/resources/base/element/string.json similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/element/string.json rename to src/apps/ohos/entry/src/main/resources/base/element/string.json diff --git a/src/apps/vcoder/entry/src/main/resources/base/media/background.png b/src/apps/ohos/entry/src/main/resources/base/media/background.png similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/media/background.png rename to src/apps/ohos/entry/src/main/resources/base/media/background.png diff --git a/src/apps/vcoder/entry/src/main/resources/base/media/bitfun_icon.png b/src/apps/ohos/entry/src/main/resources/base/media/bitfun_icon.png similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/media/bitfun_icon.png rename to src/apps/ohos/entry/src/main/resources/base/media/bitfun_icon.png diff --git a/src/apps/vcoder/entry/src/main/resources/base/media/foreground.png b/src/apps/ohos/entry/src/main/resources/base/media/foreground.png similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/media/foreground.png rename to src/apps/ohos/entry/src/main/resources/base/media/foreground.png diff --git a/src/apps/vcoder/entry/src/main/resources/base/media/layered_image.json b/src/apps/ohos/entry/src/main/resources/base/media/layered_image.json similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/media/layered_image.json rename to src/apps/ohos/entry/src/main/resources/base/media/layered_image.json diff --git a/src/apps/vcoder/entry/src/main/resources/base/media/startIcon.png b/src/apps/ohos/entry/src/main/resources/base/media/startIcon.png similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/media/startIcon.png rename to src/apps/ohos/entry/src/main/resources/base/media/startIcon.png diff --git a/src/apps/vcoder/entry/src/main/resources/base/profile/backup_config.json b/src/apps/ohos/entry/src/main/resources/base/profile/backup_config.json similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/profile/backup_config.json rename to src/apps/ohos/entry/src/main/resources/base/profile/backup_config.json diff --git a/src/apps/vcoder/entry/src/main/resources/base/profile/main_pages.json b/src/apps/ohos/entry/src/main/resources/base/profile/main_pages.json similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/base/profile/main_pages.json rename to src/apps/ohos/entry/src/main/resources/base/profile/main_pages.json diff --git a/src/apps/vcoder/entry/src/main/resources/dark/element/color.json b/src/apps/ohos/entry/src/main/resources/dark/element/color.json similarity index 100% rename from src/apps/vcoder/entry/src/main/resources/dark/element/color.json rename to src/apps/ohos/entry/src/main/resources/dark/element/color.json diff --git a/src/apps/vcoder/entry/src/mock/mock-config.json5 b/src/apps/ohos/entry/src/mock/mock-config.json5 similarity index 100% rename from src/apps/vcoder/entry/src/mock/mock-config.json5 rename to src/apps/ohos/entry/src/mock/mock-config.json5 diff --git a/src/apps/vcoder/entry/src/ohosTest/ets/test/Ability.test.ets b/src/apps/ohos/entry/src/ohosTest/ets/test/Ability.test.ets similarity index 100% rename from src/apps/vcoder/entry/src/ohosTest/ets/test/Ability.test.ets rename to src/apps/ohos/entry/src/ohosTest/ets/test/Ability.test.ets diff --git a/src/apps/vcoder/entry/src/ohosTest/ets/test/List.test.ets b/src/apps/ohos/entry/src/ohosTest/ets/test/List.test.ets similarity index 100% rename from src/apps/vcoder/entry/src/ohosTest/ets/test/List.test.ets rename to src/apps/ohos/entry/src/ohosTest/ets/test/List.test.ets diff --git a/src/apps/vcoder/entry/src/ohosTest/module.json5 b/src/apps/ohos/entry/src/ohosTest/module.json5 similarity index 100% rename from src/apps/vcoder/entry/src/ohosTest/module.json5 rename to src/apps/ohos/entry/src/ohosTest/module.json5 diff --git a/src/apps/vcoder/entry/src/test/List.test.ets b/src/apps/ohos/entry/src/test/List.test.ets similarity index 100% rename from src/apps/vcoder/entry/src/test/List.test.ets rename to src/apps/ohos/entry/src/test/List.test.ets diff --git a/src/apps/vcoder/entry/src/test/LocalUnit.test.ets b/src/apps/ohos/entry/src/test/LocalUnit.test.ets similarity index 100% rename from src/apps/vcoder/entry/src/test/LocalUnit.test.ets rename to src/apps/ohos/entry/src/test/LocalUnit.test.ets diff --git a/src/apps/vcoder/hvigor/hvigor-config.json5 b/src/apps/ohos/hvigor/hvigor-config.json5 similarity index 100% rename from src/apps/vcoder/hvigor/hvigor-config.json5 rename to src/apps/ohos/hvigor/hvigor-config.json5 diff --git a/src/apps/vcoder/hvigorfile.ts b/src/apps/ohos/hvigorfile.ts similarity index 100% rename from src/apps/vcoder/hvigorfile.ts rename to src/apps/ohos/hvigorfile.ts diff --git a/src/apps/vcoder/oh-package-lock.json5 b/src/apps/ohos/oh-package-lock.json5 similarity index 100% rename from src/apps/vcoder/oh-package-lock.json5 rename to src/apps/ohos/oh-package-lock.json5 diff --git a/src/apps/vcoder/oh-package.json5 b/src/apps/ohos/oh-package.json5 similarity index 100% rename from src/apps/vcoder/oh-package.json5 rename to src/apps/ohos/oh-package.json5 diff --git a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx index e1dd1d7b0..113696577 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx @@ -24,6 +24,9 @@ export interface CompactToolCardProps { isExpanded?: boolean; /** Card click callback */ onClick?: (e: React.MouseEvent) => void; + onMouseDown?: (e: React.MouseEvent) => void; + onMouseUp?: (e: React.MouseEvent) => void; + onMouseMove?: (e: React.MouseEvent) => void; /** Custom class name */ className?: string; /** Whether clickable */ @@ -37,18 +40,20 @@ export interface CompactToolCardProps { export const CompactToolCard: React.FC = ({ status, isExpanded = false, - onClick, + onMouseDown, + onMouseUp, + onMouseMove, className = '', clickable = false, header, expandedContent, }) => { const handleWrapperClick = (event: React.MouseEvent) => { - if (!onClick || shouldIgnoreCardToggleClick(event)) { + if (!onMouseUp || shouldIgnoreCardToggleClick(event)) { return; } - onClick(event); + onMouseUp(event); }; const loadingShimmer = @@ -67,7 +72,7 @@ export const CompactToolCard: React.FC = ({ className={`compact-tool-card-wrapper--expanded-card ${className}`.trim()} header={header} expandedContent={expandedContent} - headerExpandAffordance={clickable || Boolean(onClick)} + headerExpandAffordance={clickable || Boolean(onMouseUp)} /> ); } @@ -78,7 +83,9 @@ export const CompactToolCard: React.FC = ({ >
{header} diff --git a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx index ec13a8b90..8a5023cd8 100644 --- a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx @@ -93,6 +93,7 @@ export const DefaultToolCard: React.FC = ({ const { t } = useTranslation('flow-chat'); const { toolCall, toolResult, status, requiresConfirmation, userConfirmed } = toolItem; const [isExpanded, setIsExpanded] = useState(false); + const [shouldExpand, setShouldExpand] = useState(true); const toolId = toolItem.id ?? toolCall?.id; const { cardRootRef, applyExpandedState } = useToolCardHeightContract({ toolId, @@ -128,14 +129,25 @@ export const DefaultToolCard: React.FC = ({ onReject?.(); }; + const handleMouseDown = useCallback(() => { + setShouldExpand(true); + }, [applyExpandedState, canExpand, isExpanded, onExpand,shouldExpand, setShouldExpand]); + + const handleMouseMove = useCallback(() => { + setShouldExpand(false); + }, [applyExpandedState, canExpand, isExpanded, onExpand,shouldExpand, setShouldExpand]); + const handleToggleExpand = useCallback(() => { if (!canExpand) return; const nextExpanded = !isExpanded; - applyExpandedState(isExpanded, nextExpanded, setIsExpanded, { - onExpand, + if (shouldExpand) { + applyExpandedState(isExpanded, nextExpanded, setIsExpanded, { + onExpand, }); - }, [applyExpandedState, canExpand, isExpanded, onExpand]); + } + setShouldExpand(true); + }, [applyExpandedState, canExpand, isExpanded, onExpand, shouldExpand, setShouldExpand]); const getStatusText = () => { if (requiresConfirmation && !userConfirmed) { @@ -213,7 +225,9 @@ export const DefaultToolCard: React.FC = ({ = ({ return t('toolCards.git.executionFailed'); }; + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseDown = useCallback(() => { + setShouldExpand(true); + }, [hasOutput, isFailed, toggleExpanded, shouldExpand, setShouldExpand]); + + const handleMouseMove = useCallback(() => { + setShouldExpand(false); + }, [hasOutput, isFailed, toggleExpanded, shouldExpand, setShouldExpand]); + const handleCardClick = useCallback((e: React.MouseEvent) => { const target = e.target as HTMLElement; if (target.closest('.tool-card-header-actions, .git-action-buttons, .terminal-header-actions')) { return; } - if (hasOutput || isFailed) { + if ((hasOutput || isFailed) && shouldExpand) { toggleExpanded(); } - }, [hasOutput, isFailed, toggleExpanded]); + setShouldExpand(true); + }, [hasOutput, isFailed, toggleExpanded, shouldExpand, setShouldExpand]); const renderStatusIcon = () => { if (isLoading) { @@ -417,7 +428,9 @@ export const GitToolDisplay: React.FC = ({ = ({ const searchPath = getSearchPath(); const hasDetails = status === 'completed' && files.length > 0; const hasResultData = toolResult?.result !== undefined && toolResult?.result !== null; + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseMove= useCallback(() => { + setShouldExpand(false); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); + + const handleMouseDown = useCallback(() => { + setShouldExpand(true); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); const handleClick = useCallback(() => { - if (hasDetails) { + if (hasDetails && shouldExpand) { applyExpandedState(isExpanded, !isExpanded, setIsExpanded, { onExpand, }); } - }, [applyExpandedState, hasDetails, isExpanded, onExpand]); + setShouldExpand(true); + }, [applyExpandedState, hasDetails, isExpanded, onExpand,shouldExpand, setShouldExpand]); const renderContent = () => { if (status === 'completed') { @@ -180,7 +190,9 @@ export const GlobSearchDisplay: React.FC = ({ = ({ const hasDetails = status === 'completed' && stats.matches > 0; const hasResultData = toolResult?.result !== undefined && toolResult?.result !== null; + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseMove = useCallback(() => { + setShouldExpand(false); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); + + const handleMouseDown = useCallback(() => { + setShouldExpand(true); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); + const handleClick = useCallback(() => { - if (hasDetails) { + if (hasDetails && shouldExpand) { applyExpandedState(isExpanded, !isExpanded, setIsExpanded, { onExpand, }); } - }, [applyExpandedState, hasDetails, isExpanded, onExpand]); + setShouldExpand(true); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); const renderContent = () => { if (status === 'completed') { @@ -135,7 +146,9 @@ export const GrepSearchDisplay: React.FC = ({ = ({ const hasDetails = status === 'completed' && entries.length > 0; const hasResultData = toolResult?.result !== undefined && toolResult?.result !== null; + const [shouldExpand, setShouldExpand] = useState(true); + const handleMouseDown = useCallback(() => { + setShouldExpand(true); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); + + const handleMouseMove = useCallback(() => { + setShouldExpand(false); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); const handleClick = useCallback(() => { - if (hasDetails) { + if (hasDetails && shouldExpand) { applyExpandedState(isExpanded, !isExpanded, setIsExpanded, { onExpand, }); } - }, [applyExpandedState, hasDetails, isExpanded, onExpand]); + }, [applyExpandedState, hasDetails, isExpanded, onExpand, shouldExpand, setShouldExpand]); const renderContent = () => { if (status === 'completed') { @@ -176,7 +184,9 @@ export const LSDisplay: React.FC = ({ = ({ thin return t('toolCards.think.thinkingCharacters', { count: content.length }); }, [content, t]); + const shouldExpand = useRef(true); + + const handleMouseDown = () => { + shouldExpand.current = true; + }; + + const handleMouseMove = () => { + shouldExpand.current = false; + }; + const handleToggleClick = () => { - const nextExpanded = !isExpanded; - userToggledRef.current = true; - applyExpandedState(isExpanded, nextExpanded, setIsExpanded); + if (shouldExpand.current) { + const nextExpanded = !isExpanded; + userToggledRef.current = true; + applyExpandedState(isExpanded, nextExpanded, setIsExpanded); + } + shouldExpand.current = true; }; const headerLabel = (isExpanded @@ -113,7 +126,9 @@ export const ModelThinkingDisplay: React.FC = ({ thin
{headerLabel} diff --git a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx index d1ee33108..707e17631 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx @@ -2,7 +2,7 @@ * Compact display for the read_file tool. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Check, FileText, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { IconButton } from '../../component-library'; @@ -152,6 +152,23 @@ export const ReadFileDisplay: React.FC = React.memo(({ return null; }; + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseDown = () => { + setShouldExpand(true); + } + + const handleMouseMove = () => { + setShouldExpand(false); + } + + const handleMouseUp = () => { + if (shouldExpand && canOpenFile) { + handleOpenInEditor(); + } + setShouldExpand(true); + } + const renderActions = () => { if (!showConfirmationActions) { return undefined; @@ -203,7 +220,9 @@ export const ReadFileDisplay: React.FC = React.memo(({ canOpenFile && handleOpenInEditor()} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} className="read-file-card" clickable={canOpenFile} header={ diff --git a/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx index a95a95ede..3495d465c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx @@ -101,6 +101,23 @@ export const SessionControlToolCard: React.FC = React.memo(({ } }; + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseDown = () => { + setShouldExpand(true); + } + + const handleMouseMove = () => { + setShouldExpand(false); + } + + const handleMouseUp = () => { + if (shouldExpand && hasDetails) { + applyExpandedState(isExpanded, !isExpanded, setIsExpanded); + } + setShouldExpand(true); + } + const renderContent = () => { const label = getActionLabel(); @@ -261,11 +278,9 @@ export const SessionControlToolCard: React.FC = React.memo(({ { - if (hasDetails) { - applyExpandedState(isExpanded, !isExpanded, setIsExpanded); - } - }} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} className="session-control-card" clickable={hasDetails} header={( diff --git a/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx index 21ee458a5..3f817b392 100644 --- a/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx @@ -60,6 +60,23 @@ export const SessionMessageToolCard: React.FC = React.memo(({ const targetLabel = targetSessionId || t('toolCards.sessionMessage.unknownSession'); + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseDown = () => { + setShouldExpand(true); + } + + const handleMouseMove = () => { + setShouldExpand(false); + } + + const handleMouseUp = () => { + if (shouldExpand && hasDetails) { + applyExpandedState(isExpanded, !isExpanded, setIsExpanded); + } + setShouldExpand(true); + } + const renderContent = () => { if (status === 'completed') { return <>{t('toolCards.sessionMessage.messageAccepted', { session: targetLabel })}; @@ -131,11 +148,9 @@ export const SessionMessageToolCard: React.FC = React.memo(({ { - if (hasDetails) { - applyExpandedState(isExpanded, !isExpanded, setIsExpanded); - } - }} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} className="session-message-card" clickable={hasDetails} header={( diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx index 646b30fe4..339958a15 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx @@ -430,14 +430,26 @@ export const TerminalToolCard: React.FC = ({ createTerminalTab(terminalSessionId, terminalName); }, [terminalSessionId]); + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseDown = useCallback(() => { + setShouldExpand(true); + }, [toggleExpanded, shouldExpand, setShouldExpand]); + + const handleMouseMove = useCallback(() => { + setShouldExpand(false); + }, [toggleExpanded, shouldExpand, setShouldExpand]); + const handleCardClick = useCallback((e: React.MouseEvent) => { const target = e.target as HTMLElement; if (target.closest('.tool-card-header-actions, .terminal-action-btn, .terminal-confirm-actions')) { return; } - - toggleExpanded(); - }, [toggleExpanded]); + if (shouldExpand) { + toggleExpanded(); + } + setShouldExpand(true); + }, [toggleExpanded, shouldExpand, setShouldExpand]); const renderLoadingStatusIcon = () => { if (viewState.isLoading) { @@ -659,7 +671,9 @@ export const TerminalToolCard: React.FC = ({ = ({ const hasSummary = !hasResults && searchResults && searchResults.summary; const isExpandable = status === 'completed' && (hasResults || hasSummary); + const [shouldExpand, setShouldExpand] = useState(true); + + const handleMouseDown= useCallback(() => { + setShouldExpand(true); + }, [applyExpandedState, isExpandable, isExpanded, onExpand, shouldExpand, setShouldExpand]); + + const handleMouseMove = useCallback(() => { + setShouldExpand(false) + }, [applyExpandedState, isExpandable, isExpanded, onExpand, shouldExpand, setShouldExpand]); + const handleClick = useCallback(() => { - if (isExpandable) { + if (isExpandable && shouldExpand) { applyExpandedState(isExpanded, !isExpanded, setIsExpanded, { onExpand, }); } - }, [applyExpandedState, isExpandable, isExpanded, onExpand]); + setShouldExpand(true); + }, [applyExpandedState, isExpandable, isExpanded, onExpand, shouldExpand, setShouldExpand]); const renderContent = () => { if (status === 'completed') { @@ -163,7 +174,9 @@ export const WebSearchCard: React.FC = ({ Date: Tue, 19 May 2026 21:06:42 +0800 Subject: [PATCH 31/31] add copy menu bug --- .../src/app/components/panels/base/FlexiblePanel.tsx | 1 + src/web-ui/src/app/scenes/terminal/TerminalScene.tsx | 1 + src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx | 1 + .../src/flow_chat/tool-cards/ToolCardHeaderActions.tsx | 3 +++ src/web-ui/src/flow_chat/tool-cards/useCopyTextAction.ts | 9 +++++++-- .../shared/context-menu-system/core/ContextResolver.ts | 5 +++-- .../providers/TerminalMenuProvider.ts | 7 +++++-- .../shared/context-menu-system/types/context.types.ts | 2 ++ .../src/tools/terminal/components/ConnectedTerminal.tsx | 3 +++ src/web-ui/src/tools/terminal/components/Terminal.tsx | 3 +++ 10 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx index a046bced2..f668d8d36 100644 --- a/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx +++ b/src/web-ui/src/app/components/panels/base/FlexiblePanel.tsx @@ -713,6 +713,7 @@ const FlexiblePanel: React.FC = memo(({ key={sessionId} sessionId={sessionId} autoFocus={true} + supportsCopyPaste={false} />
diff --git a/src/web-ui/src/app/scenes/terminal/TerminalScene.tsx b/src/web-ui/src/app/scenes/terminal/TerminalScene.tsx index 87deb411c..b2f4a0a8d 100644 --- a/src/web-ui/src/app/scenes/terminal/TerminalScene.tsx +++ b/src/web-ui/src/app/scenes/terminal/TerminalScene.tsx @@ -44,6 +44,7 @@ const TerminalScene: React.FC = ({ isActive = true }) => { showStatusBar onExit={handleExit} onClose={handleClose} + supportsCopyPaste={false} /> ) : (
diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx index 339958a15..47fcddac0 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx @@ -487,6 +487,7 @@ export const TerminalToolCard: React.FC = ({ successMessage={t('toolCards.terminal.commandCopied')} failureMessage={t('toolCards.terminal.copyCommandFailed')} ariaLabel={t('toolCards.terminal.copyCommand')} + showSuccessNotification={false} /> ); diff --git a/src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderActions.tsx b/src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderActions.tsx index 74c17a4f5..3f1561d6c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderActions.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ToolCardHeaderActions.tsx @@ -30,6 +30,7 @@ interface ToolCardCopyActionProps { ariaLabel?: string; className?: string; disabled?: boolean; + showSuccessNotification?: boolean; } export const ToolCardCopyAction: React.FC = ({ @@ -41,11 +42,13 @@ export const ToolCardCopyAction: React.FC = ({ ariaLabel, className, disabled, + showSuccessNotification }) => { const { copied, copy } = useCopyTextAction({ getText, successMessage, failureMessage, + showSuccessNotification }); return ( diff --git a/src/web-ui/src/flow_chat/tool-cards/useCopyTextAction.ts b/src/web-ui/src/flow_chat/tool-cards/useCopyTextAction.ts index 235639a30..d33c2e234 100644 --- a/src/web-ui/src/flow_chat/tool-cards/useCopyTextAction.ts +++ b/src/web-ui/src/flow_chat/tool-cards/useCopyTextAction.ts @@ -8,6 +8,7 @@ interface UseCopyTextActionOptions { successMessage: string; failureMessage: string; resetMs?: number; + showSuccessNotification?: boolean; } export function useCopyTextAction({ @@ -15,6 +16,7 @@ export function useCopyTextAction({ successMessage, failureMessage, resetMs = 1600, + showSuccessNotification = true, }: UseCopyTextActionOptions) { const [copied, setCopied] = useState(false); const resetTimerRef = useRef(null); @@ -42,7 +44,10 @@ export function useCopyTextAction({ } setCopied(true); - notificationService.success(successMessage, { duration: resetMs }); + if (showSuccessNotification){ + notificationService.success(successMessage, { duration: resetMs }); + } + if (resetTimerRef.current !== null) { window.clearTimeout(resetTimerRef.current); @@ -51,7 +56,7 @@ export function useCopyTextAction({ setCopied(false); resetTimerRef.current = null; }, resetMs); - }, [failureMessage, getText, resetMs, successMessage]); + }, [failureMessage, getText, resetMs, successMessage,showSuccessNotification]); return { copied, copy }; } diff --git a/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts b/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts index b543f9be3..8e6a2639c 100644 --- a/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts +++ b/src/web-ui/src/shared/context-menu-system/core/ContextResolver.ts @@ -130,7 +130,7 @@ export class ContextResolver { `terminal-${Date.now()}`; const sessionId = terminalElement.getAttribute('data-session-id') || undefined; const isReadOnly = terminalElement.getAttribute('data-readonly') === 'true'; - + const supportsCopyPaste = terminalElement.getAttribute('data-supports-copy-paste') !== 'false'; const selection = window.getSelection(); const selectedText = selection?.toString() || ''; @@ -143,7 +143,8 @@ export class ContextResolver { sessionId, hasSelection, selectedText: hasSelection ? selectedText : undefined, - isReadOnly + isReadOnly, + supportsCopyPaste }; } diff --git a/src/web-ui/src/shared/context-menu-system/providers/TerminalMenuProvider.ts b/src/web-ui/src/shared/context-menu-system/providers/TerminalMenuProvider.ts index 46e452c44..4d12b9803 100644 --- a/src/web-ui/src/shared/context-menu-system/providers/TerminalMenuProvider.ts +++ b/src/web-ui/src/shared/context-menu-system/providers/TerminalMenuProvider.ts @@ -19,9 +19,10 @@ export class TerminalMenuProvider implements IMenuProvider { const terminalContext = context as TerminalContext; const items: MenuItem[] = []; const isReadOnly = terminalContext.isReadOnly ?? false; - + const supportsCopyPaste = terminalContext.supportsCopyPaste ?? true; - items.push({ + if(supportsCopyPaste) { + items.push({ id: 'terminal-copy', label: i18nService.t('common:actions.copy'), icon: 'Copy', @@ -51,6 +52,8 @@ export class TerminalMenuProvider implements IMenuProvider { } }); } + } + items.push({ diff --git a/src/web-ui/src/shared/context-menu-system/types/context.types.ts b/src/web-ui/src/shared/context-menu-system/types/context.types.ts index 66adf1cd4..ca4e7b960 100644 --- a/src/web-ui/src/shared/context-menu-system/types/context.types.ts +++ b/src/web-ui/src/shared/context-menu-system/types/context.types.ts @@ -122,6 +122,8 @@ export interface TerminalContext extends BaseContext { selectedText?: string; isReadOnly?: boolean; + + supportsCopyPaste?: boolean; } diff --git a/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx b/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx index 511d88fc5..d4c18a45e 100644 --- a/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx +++ b/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx @@ -37,6 +37,7 @@ export interface ConnectedTerminalProps { onClose?: () => void; onTitleChange?: (title: string) => void; onExit?: (exitCode?: number) => void; + supportsCopyPaste?: boolean; } const ConnectedTerminal: React.FC = memo(({ @@ -49,6 +50,7 @@ const ConnectedTerminal: React.FC = memo(({ onClose, onTitleChange, onExit, + supportsCopyPaste = true, }) => { const terminalRef = useRef(null); const [title, setTitle] = useState(initialSession?.name || 'Terminal'); @@ -412,6 +414,7 @@ const ConnectedTerminal: React.FC = memo(({ onReady={handleTerminalReady} onPaste={handlePaste} preventShrinkBelowColsRef={preventShrinkBelowColsRef} + supportsCopyPaste={supportsCopyPaste} /> {showStatusBar && session && ( diff --git a/src/web-ui/src/tools/terminal/components/Terminal.tsx b/src/web-ui/src/tools/terminal/components/Terminal.tsx index 9b11e65f7..313fcfcae 100644 --- a/src/web-ui/src/tools/terminal/components/Terminal.tsx +++ b/src/web-ui/src/tools/terminal/components/Terminal.tsx @@ -130,6 +130,7 @@ export interface TerminalProps { * content. Set back to 0 (or leave unset) to restore normal resize behaviour. */ preventShrinkBelowColsRef?: React.MutableRefObject; + supportsCopyPaste?: boolean; } export interface TerminalRef { @@ -179,6 +180,7 @@ const Terminal = forwardRef(({ onReady, onPaste, preventShrinkBelowColsRef, + supportsCopyPaste = true, }, ref) => { const containerRef = useRef(null); const terminalRef = useRef(null); @@ -684,6 +686,7 @@ const Terminal = forwardRef(({ className={`bitfun-terminal ${className}`} data-terminal-id={terminalId} data-session-id={sessionId} + data-supports-copy-paste={supportsCopyPaste?'true':'false'} >