4 Commits
v0.9.1 ... main

Author SHA1 Message Date
SteveLauC
65a48efdde fix: implement custom serialization for Extension.minimum_coco_version (#1010)
Coco's version string does not adhere to semver's spec, so we had to
write a custom deserialization impl to do the conversion:

```
0.9.1-SNPASHOT-2560 => 0.9.1-SNAPSHOT.2560
```

But we forget to serialize versions to Coco's format when writing
extensions to disk. When Coco reads extension from disk, it sees
valid semantic versions, which are not expected:

```
[WAR] [third_party:167] invalid extension: [base64-converter]:
  field [minimum_coco_version] has invalid version:
    failed to parse build number 'SNAPSHOT.2560'', caused by: ['invalid digit found in string']
```

This commit provides a custom serialization impl to fix the issue.
2025-12-16 15:29:36 +08:00
ayangweb
0613238876 refactor: hide the filter bar in quick ai access (#1008) 2025-12-15 09:25:13 +08:00
SteveLauC
501f6df473 chore: show error msg (not err code) when installing exts via deeplink/store fails (#1007)
* chore: show error msg (not err code) when installing exts via deeplink fails

When installing extensions via deeplink fails, previous implementation
showed the raw error code returned from the backend interfaces, which
is not user-friendly. We now call installExtensionError() to interrupt
the error code to get a human-readable error message, then show it to
the users.

* fix: correct install extension error when installing via store
2025-12-14 09:24:13 +08:00
ayangweb
67c8c4bdfa fix: fix the abnormal input height issue (#1006)
* fix: fix the abnormal input height issue

* docs: update changelog
2025-12-09 15:07:41 +08:00
6 changed files with 108 additions and 36 deletions

View File

@@ -7,11 +7,20 @@ title: "Release Notes"
Information about release notes of Coco App is provided here.
## Latest (In development)
### ❌ Breaking changes
### 🚀 Features
### 🐛 Bug fix
### ✈️ Improvements
## Latest (In development)
### ❌ Breaking changes
### 🚀 Features
### 🐛 Bug fix
- fix: fix the abnormal input height issue #1006
- fix: implement custom serialization for Extension.minimum_coco_version #1010
### ✈️ Improvements
- chore: show error msg (not err code) when installing exts via deeplink fails #1007
## 0.9.1 (2025-12-05)

View File

@@ -135,7 +135,9 @@ pub struct Extension {
/// It is only for third-party extensions. Built-in extensions should always
/// set this field to `None`.
#[serde(deserialize_with = "deserialize_coco_semver")]
#[serde(default)] // None if this field is missing
#[serde(serialize_with = "serialize_coco_semver")]
// None if this field is missing, required as we use custom deserilize method.
#[serde(default)]
minimum_coco_version: Option<SemVer>,
/*
@@ -419,6 +421,37 @@ where
Ok(Some(semver))
}
/// Serialize Coco SemVer to a string.
///
/// For a `SemVer`, there are 2 possible input cases, guarded by `to_semver()`:
///
/// 1. "x.y.z" => "x.y.z"
/// 2. "x.y.z-SNAPSHOT.2560" => "x.y.z-SNAPSHOT-2560"
fn serialize_coco_semver<S>(version: &Option<SemVer>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match version {
Some(v) => {
assert!(v.build.is_empty());
let s = if v.pre.is_empty() {
format!("{}.{}.{}", v.major, v.minor, v.patch)
} else {
format!(
"{}.{}.{}-{}",
v.major,
v.minor,
v.patch,
v.pre.as_str().replace('.', "-")
)
};
serializer.serialize_str(&s)
}
None => serializer.serialize_none(),
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub(crate) struct CommandAction {
pub(crate) exec: String,
@@ -2150,4 +2183,31 @@ mod tests {
let deserialized: FileSystemAccess = serde_json::from_str(&serialized).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_serialize_coco_semver_none() {
let version: Option<SemVer> = None;
let mut serializer = serde_json::Serializer::new(Vec::new());
serialize_coco_semver(&version, &mut serializer).unwrap();
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
assert_eq!(serialized, "null");
}
#[test]
fn test_serialize_coco_semver_simple() {
let version: Option<SemVer> = Some(SemVer::parse("1.2.3").unwrap());
let mut serializer = serde_json::Serializer::new(Vec::new());
serialize_coco_semver(&version, &mut serializer).unwrap();
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
assert_eq!(serialized, "\"1.2.3\"");
}
#[test]
fn test_serialize_coco_semver_with_pre() {
let version: Option<SemVer> = Some(SemVer::parse("1.2.3-SNAPSHOT.1234").unwrap());
let mut serializer = serde_json::Serializer::new(Vec::new());
serialize_coco_semver(&version, &mut serializer).unwrap();
let serialized = String::from_utf8(serializer.into_inner()).unwrap();
assert_eq!(serialized, "\"1.2.3-SNAPSHOT-1234\"");
}
}

View File

@@ -244,7 +244,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
"info"
);
} catch (error) {
installExtensionError(String(error));
installExtensionError(error);
} finally {
const { installingExtensions } = useSearchStore.getState();
@@ -306,7 +306,7 @@ const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
<div
key={id}
className={clsx(
"flex justify-between gap-4 h-[40px] px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition",
"flex justify-between gap-4 h-10 px-2 rounded-lg cursor-pointer text-[#333] dark:text-[#d8d8d8] transition",
{
"bg-black/10 dark:bg-white/15":
selectedExtension?.id === id,

View File

@@ -36,7 +36,7 @@ const MultilevelWrapper: FC<MultilevelWrapperProps> = (props) => {
<div
data-tauri-drag-region
className={clsx(
"flex items-center h-[40px] gap-1 px-2 border border-[#EDEDED] dark:border-[#202126] rounded-l-lg",
"flex items-center h-10 gap-1 px-2 border border-[#EDEDED] dark:border-[#202126] rounded-l-lg",
{
"justify-center": visibleSearchBar(),
"w-[calc(100vw-16px)] rounded-r-lg": !visibleSearchBar(),
@@ -115,7 +115,7 @@ export default function SearchIcons({
}
return (
<div className="flex items-center justify-center pl-2 h-[40px] bg-[#ededed] dark:bg-[#202126]">
<div className="flex items-center justify-center pl-2 h-10 bg-[#ededed] dark:bg-[#202126]">
<Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" />
</div>
);

View File

@@ -12,6 +12,7 @@ import platformAdapter from "@/utils/platformAdapter";
import { useTranslation } from "react-i18next";
import { MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL } from "@/constants";
import { useAsyncEffect, useEventListener } from "ahooks";
import { installExtensionError } from "@/utils";
export interface DeepLinkHandler {
pattern: string;
@@ -78,7 +79,7 @@ export function useDeepLinkManager() {
addError(t("deepLink.extensionInstallSuccessfully"), "info");
console.log("Extension installed successfully:", extensionId);
} catch (error) {
addError(String(error));
installExtensionError(error)
}
}, []);

View File

@@ -305,10 +305,10 @@ export const visibleSearchBar = () => {
};
export const visibleFilterBar = () => {
const { viewExtensionOpened, visibleExtensionDetail } =
const { viewExtensionOpened, visibleExtensionDetail, goAskAi } =
useSearchStore.getState();
if (visibleExtensionDetail) return false;
if (visibleExtensionDetail || goAskAi) return false;
if (isNil(viewExtensionOpened)) return true;
@@ -329,7 +329,7 @@ export const visibleFooterBar = () => {
export const installExtensionError = (error: any) => {
console.log(error);
const { addError } = useAppStore.getState();
let message = "settings.extensions.hints.importFailed";
@@ -338,8 +338,7 @@ export const installExtensionError = (error: any) => {
message = "settings.extensions.hints.extensionAlreadyImported";
}
if ( isObject(error) && "IncompatiblePlatform" in error
) {
if (isObject(error) && "IncompatiblePlatform" in error) {
message = "settings.extensions.hints.platformIncompatibleExtension";
}
@@ -367,27 +366,30 @@ export const installExtensionError = (error: any) => {
} else if ("InvalidPluginJson" in source) {
const innerSource = (source as any).InvalidPluginJson.source;
const kind = innerSource.kind;
if (isObject(kind)) {
if ("DuplicateSubExtensionId" in kind) {
message = "settings.extensions.hints.duplicateSubExtensionId";
options = (kind as any).DuplicateSubExtensionId;
} else if ("FieldsNotAllowed" in kind) {
message = "settings.extensions.hints.fieldsNotAllowed";
options = (kind as any).FieldsNotAllowed;
} else if ("FieldsNotAllowedForSubExtension" in kind) {
message = "settings.extensions.hints.fieldsNotAllowedForSubExtension";
options = (kind as any).FieldsNotAllowedForSubExtension;
} else if ("TypesNotAllowedForSubExtension" in kind) {
message = "settings.extensions.hints.typesNotAllowedForSubExtension";
options = (kind as any).TypesNotAllowedForSubExtension;
} else if ("SubExtensionHasMoreSupportedPlatforms" in kind) {
message = "settings.extensions.hints.subExtensionHasMoreSupportedPlatforms";
options = (kind as any).SubExtensionHasMoreSupportedPlatforms;
} else if ("FieldRequired" in kind) {
message = "settings.extensions.hints.fieldRequired";
options = (kind as any).FieldRequired;
}
if ("DuplicateSubExtensionId" in kind) {
message = "settings.extensions.hints.duplicateSubExtensionId";
options = (kind as any).DuplicateSubExtensionId;
} else if ("FieldsNotAllowed" in kind) {
message = "settings.extensions.hints.fieldsNotAllowed";
options = (kind as any).FieldsNotAllowed;
} else if ("FieldsNotAllowedForSubExtension" in kind) {
message =
"settings.extensions.hints.fieldsNotAllowedForSubExtension";
options = (kind as any).FieldsNotAllowedForSubExtension;
} else if ("TypesNotAllowedForSubExtension" in kind) {
message =
"settings.extensions.hints.typesNotAllowedForSubExtension";
options = (kind as any).TypesNotAllowedForSubExtension;
} else if ("SubExtensionHasMoreSupportedPlatforms" in kind) {
message =
"settings.extensions.hints.subExtensionHasMoreSupportedPlatforms";
options = (kind as any).SubExtensionHasMoreSupportedPlatforms;
} else if ("FieldRequired" in kind) {
message = "settings.extensions.hints.fieldRequired";
options = (kind as any).FieldRequired;
}
}
}
} else if (source === "MissingPluginJson") {