8 Commits

Author SHA1 Message Date
ayang
e7f8f7ef6e chore: add macos config for tauri 2025-07-28 16:31:54 +08:00
ayangweb
4709f8c660 feat: enhance ui for skipped version (#834) 2025-07-28 11:43:10 +08:00
SteveLauC
4696aa1759 test: test extract_build_number() (#835)
This commit adds a test for extract_build_number(), which I forgot to do
in commit 067fb7144f6[1].

[1]: 067fb7144f
2025-07-28 11:42:50 +08:00
ayangweb
924fc09516 fix: fix issue with update check failure (#833)
* fix: fix issue with update check failure

* docs: update changelog
2025-07-28 10:06:07 +08:00
SteveLauC
5a700662dd chore: release notes for 0.7.1 (#832) 2025-07-28 10:00:12 +08:00
BiggerRain
8f992bfa92 chore: bump version number to 0.7.1 (#830) 2025-07-27 17:26:08 +08:00
BiggerRain
e7dd27c744 chore: add toggle_move_to_active_space_attribute (#829)
* chore: add toggle_move_to_active_space_attribute

* chore: pin

* chore: add

* update

---------

Co-authored-by: Steve Lau <stevelauc@outlook.com>
2025-07-27 16:50:11 +08:00
ayangweb
7914836c3e fix: correct enter key behavior (#828) 2025-07-27 11:52:40 +08:00
16 changed files with 258 additions and 85 deletions

View File

@@ -5,22 +5,36 @@ title: "Release Notes"
# Release Notes
Information about release notes of Coco Server is provided here.
Information about release notes of Coco App is provided here.
## Latest (In development)
### ❌ Breaking changes
### 🚀 Features
- feat: enhance ui for skipped version #834
### 🐛 Bug fix
- fix: fix issue with update check failure #833
### ✈️ Improvements
## 0.7.1 (2025-07-27)
### ❌ Breaking changes
### 🚀 Features
### 🐛 Bug fix
- fix: correct enter key behavior #828
### ✈️ Improvements
- chore: web component add notification component #825
- refactor: collection behavior defaults to `MoveToActiveSpace`, and only use `CanJoinAllSpaces` when window is pinned #829
## 0.7.0 (2025-07-25)

View File

@@ -1,7 +1,7 @@
{
"name": "coco",
"private": true,
"version": "0.7.0",
"version": "0.7.1",
"type": "module",
"scripts": {
"dev": "vite",

83
src-tauri/Cargo.lock generated
View File

@@ -840,7 +840,7 @@ dependencies = [
[[package]]
name = "coco"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"anyhow",
"applications",
@@ -852,6 +852,7 @@ dependencies = [
"cfg-if",
"chinese-number",
"chrono",
"cocoa 0.24.1",
"derive_more 2.0.1",
"dirs 5.0.1",
"enigo",
@@ -914,6 +915,22 @@ dependencies = [
"zip 4.0.0",
]
[[package]]
name = "cocoa"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation 0.1.2",
"core-foundation 0.9.4",
"core-graphics 0.22.3",
"foreign-types 0.3.2",
"libc",
"objc",
]
[[package]]
name = "cocoa"
version = "0.26.0"
@@ -922,14 +939,28 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
dependencies = [
"bitflags 2.9.0",
"block",
"cocoa-foundation",
"cocoa-foundation 0.2.0",
"core-foundation 0.10.0",
"core-graphics",
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.2.0"
@@ -939,7 +970,7 @@ dependencies = [
"bitflags 2.9.0",
"block",
"core-foundation 0.10.0",
"core-graphics-types",
"core-graphics-types 0.2.0",
"libc",
"objc",
]
@@ -1056,6 +1087,19 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types 0.3.2",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.24.0"
@@ -1064,11 +1108,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.9.0",
"core-foundation 0.10.0",
"core-graphics-types",
"core-graphics-types 0.2.0",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
@@ -1472,8 +1527,8 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fd9ae1736d6ebb2e472740fbee86fb2178b8d56feb98a6751411d4c95b7e72"
dependencies = [
"cocoa",
"core-graphics",
"cocoa 0.26.0",
"core-graphics 0.24.0",
"dunce",
"gdk",
"gdkx11",
@@ -1562,7 +1617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cf6f550bbbdd5fe66f39d429cb2604bcdacbf00dca0f5bbe2e9306a0009b7c6"
dependencies = [
"core-foundation 0.10.0",
"core-graphics",
"core-graphics 0.24.0",
"foreign-types-shared 0.3.1",
"libc",
"log",
@@ -2714,7 +2769,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.59.0",
"windows-core 0.61.2",
]
[[package]]
@@ -5721,7 +5776,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"js-sys",
"log",
@@ -5889,7 +5944,7 @@ dependencies = [
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows 0.59.0",
"windows 0.61.3",
]
[[package]]
@@ -5947,7 +6002,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
dependencies = [
"bitflags 2.9.0",
"core-foundation 0.10.0",
"core-graphics",
"core-graphics 0.24.0",
"crossbeam-channel",
"dispatch",
"dlopen2",
@@ -6145,9 +6200,9 @@ source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#d4b9df797959f8fa
dependencies = [
"bitflags 2.9.0",
"block",
"cocoa",
"cocoa 0.26.0",
"core-foundation 0.10.0",
"core-graphics",
"core-graphics 0.24.0",
"objc",
"objc-foundation",
"objc_id",

View File

@@ -1,6 +1,6 @@
[package]
name = "coco"
version = "0.7.0"
version = "0.7.1"
description = "Search, connect, collaborate all in one place."
authors = ["INFINI Labs"]
edition = "2024"
@@ -109,6 +109,7 @@ sysinfo = "0.35.2"
[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
cocoa = "0.24"
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }

View File

@@ -180,6 +180,8 @@ pub fn run() {
server::synthesize::synthesize,
util::file::get_file_icon,
util::app_lang::update_app_lang,
#[cfg(target_os = "macos")]
setup::toggle_move_to_active_space_attribute,
])
.setup(|app| {
let app_handle = app.handle().clone();

View File

@@ -1,5 +1,8 @@
//credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
use tauri::{App, Emitter, EventTarget, WebviewWindow};
//! credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
use cocoa::appkit::NSWindow;
use tauri::Manager;
use tauri::{App, AppHandle, Emitter, EventTarget, WebviewWindow};
use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};
use crate::common::MAIN_WINDOW_LABEL;
@@ -29,7 +32,7 @@ pub fn platform(
// Share the window across all desktop spaces and full screen
panel.set_collection_behaviour(
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
);
@@ -78,3 +81,50 @@ pub fn platform(
// Set the delegate object for the window to handle window events
panel.set_delegate(delegate);
}
/// Change NS window attribute between `NSWindowCollectionBehaviorCanJoinAllSpaces`
/// and `NSWindowCollectionBehaviorMoveToActiveSpace` accordingly.
///
/// NOTE: this tauri command is not async because we should run it in the main
/// thread, or `ns_window.setCollectionBehavior_(collection_behavior)` would lead
/// to UB.
#[tauri::command]
pub(crate) fn toggle_move_to_active_space_attribute(tauri_app_hanlde: AppHandle) {
use cocoa::appkit::NSWindowCollectionBehavior;
use cocoa::base::id;
let main_window = tauri_app_hanlde
.get_webview_window(MAIN_WINDOW_LABEL)
.unwrap();
let ns_window = main_window.ns_window().unwrap() as id;
let mut collection_behavior = unsafe { ns_window.collectionBehavior() };
let join_all_spaces = collection_behavior
.contains(NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces);
let move_to_active_space = collection_behavior
.contains(NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace);
match (join_all_spaces, move_to_active_space) {
(true, false) => {
collection_behavior
.remove(NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces);
collection_behavior
.insert(NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace);
}
(false, true) => {
collection_behavior
.remove(NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace);
collection_behavior
.insert(NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces);
}
_ => {
panic!(
"invalid NS window attribute, NSWindowCollectionBehaviorCanJoinAllSpaces is set [{}], NSWindowCollectionBehaviorMoveToActiveSpace is set [{}]",
join_all_spaces, move_to_active_space
);
}
}
unsafe {
ns_window.setCollectionBehavior_(collection_behavior);
}
}

View File

@@ -5,7 +5,7 @@ use tauri_plugin_updater::RemoteRelease;
///
/// If the version string is in the `x.y.z` format and does not include a build
/// number, we assume a build number of 0.
fn extract_version_number(version: &Version) -> u32 {
fn extract_build_number(version: &Version) -> u32 {
let pre = &version.pre;
if pre.is_empty() {
@@ -52,8 +52,8 @@ fn extract_version_number(version: &Version) -> u32 {
pub(crate) fn custom_version_comparator(local: Version, remote_release: RemoteRelease) -> bool {
let remote = remote_release.version;
let local_build_number = extract_version_number(&local);
let remote_build_number = extract_version_number(&remote);
let local_build_number = extract_build_number(&local);
let remote_build_number = extract_build_number(&remote);
let should_update = remote_build_number > local_build_number;
log::debug!(
@@ -65,3 +65,23 @@ pub(crate) fn custom_version_comparator(local: Version, remote_release: RemoteRe
should_update
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_build_number() {
// 0.6.0 => 0
let version = Version::parse("0.6.0").unwrap();
assert_eq!(extract_build_number(&version), 0);
// 0.6.0-2371 => 2371
let version = Version::parse("0.6.0-2371").unwrap();
assert_eq!(extract_build_number(&version), 2371);
// 0.6.0-SNAPSHOT-2371 => 2371
let version = Version::parse("0.6.0-SNAPSHOT-2371").unwrap();
assert_eq!(extract_build_number(&version), 2371);
}
}

View File

@@ -1,4 +1,5 @@
{
"identifier": "rs.coco.app",
"bundle": {
"macOS": {
"entitlements": "./Entitlements.plist",
@@ -7,4 +8,4 @@
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"identifier": "rs.coco.app",
"bundle": {
"macOS": {
"entitlements": "./Entitlements.plist"
}
}
}

View File

@@ -34,4 +34,8 @@ export function show_check(): Promise<void> {
export function hide_check(): Promise<void> {
return invoke('hide_check');
}
export function toggle_move_to_active_space_attribute(): Promise<void> {
return invoke('toggle_move_to_active_space_attribute');
}

View File

@@ -7,12 +7,12 @@ import PinIcon from "@/icons/Pin";
import WindowsFullIcon from "@/icons/WindowsFull";
import { useAppStore } from "@/stores/appStore";
import type { Chat } from "@/types/chat";
import platformAdapter from "@/utils/platformAdapter";
import VisibleKey from "../Common/VisibleKey";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { HISTORY_PANEL_ID } from "@/constants";
import { AssistantList } from "./AssistantList";
import { ServerList } from "./ServerList";
import { useTogglePin } from "@/hooks/useTogglePin";
interface ChatHeaderProps {
clearChat: () => void;
@@ -35,22 +35,12 @@ export function ChatHeader({
showChatHistory = true,
assistantIDs,
}: ChatHeaderProps) {
const { isPinned, setIsPinned, isTauri } = useAppStore();
const { isTauri } = useAppStore();
const { isPinned, togglePin } = useTogglePin();
const { historicalRecords, newSession, fixedWindow, external } =
useShortcutsStore();
const togglePin = async () => {
try {
const newPinned = !isPinned;
await platformAdapter.setAlwaysOnTop(newPinned);
setIsPinned(newPinned);
} catch (err) {
console.error("Failed to toggle window pin state:", err);
setIsPinned(isPinned);
}
};
return (
<header
className="flex items-center justify-between py-2 px-3 select-none"

View File

@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { ArrowDown01, CornerDownLeft } from "lucide-react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
@@ -19,6 +19,7 @@ import source_default_dark_img from "@/assets/images/source_default_dark.png";
import { useThemeStore } from "@/stores/themeStore";
import platformAdapter from "@/utils/platformAdapter";
import FontIcon from "../Icons/FontIcon";
import { useTogglePin } from "@/hooks/useTogglePin";
interface FooterProps {
setIsPinnedWeb?: (value: boolean) => void;
@@ -37,32 +38,24 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
const isDark = useThemeStore((state) => state.isDark);
const { isTauri, isPinned, setIsPinned } = useAppStore();
const { isTauri } = useAppStore();
const { setVisible, updateInfo } = useUpdateStore();
const { isPinned, togglePin } = useTogglePin({
onPinChange: setIsPinnedWeb,
});
const { setVisible, updateInfo, skipVersions } = useUpdateStore();
const { fixedWindow, modifierKey } = useShortcutsStore();
const setWindowAlwaysOnTop = useCallback(async (isPinned: boolean) => {
setIsPinnedWeb?.(isPinned);
return platformAdapter.setAlwaysOnTop(isPinned);
}, []);
const togglePin = async () => {
try {
const newPinned = !isPinned;
await setWindowAlwaysOnTop(newPinned);
setIsPinned(newPinned);
} catch (err) {
console.error("Failed to toggle window pin state:", err);
setIsPinned(isPinned);
}
};
const openSetting = useCallback(() => {
return platformAdapter.emitEvent("open_settings", "");
}, []);
const hasUpdate = useMemo(() => {
return updateInfo && !skipVersions.includes(updateInfo.version);
}, [updateInfo, skipVersions]);
const renderLeft = () => {
if (sourceData?.source?.name) {
return (
@@ -108,7 +101,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
/>
<div className="relative text-xs text-gray-500 dark:text-gray-400">
{updateInfo?.available ? (
{hasUpdate ? (
<div className="cursor-pointer" onClick={() => setVisible(true)}>
<span>{t("search.footer.updateAvailable")}</span>
<span className="absolute top-0 -right-2 size-1.5 bg-[#FF3434] rounded-full"></span>
@@ -138,7 +131,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
onClick={togglePin}
className={clsx({
"text-blue-500": isPinned,
"pl-2": updateInfo?.available,
"pl-2": hasUpdate,
})}
>
<VisibleKey shortcut={fixedWindow} onKeyPress={togglePin}>

View File

@@ -135,12 +135,13 @@ export function useAssistantManager({
}
if (key === "Enter" && !shiftKey) {
if (!isEmpty(value)) {
e.stopPropagation();
}
e.preventDefault();
if (isTauri && !isChatMode && goAskAi) {
if (!isEmpty(value)) {
e.stopPropagation();
}
return handleAskAi();
}

View File

@@ -32,8 +32,8 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
const {
visible,
setVisible,
skipVersion,
setSkipVersion,
skipVersions,
setSkipVersions,
isOptional,
updateInfo,
setUpdateInfo,
@@ -50,11 +50,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
}, []);
useEffect(() => {
if (!snapshotUpdate) return;
checkUpdate().catch((error) => {
addError("Update failed:" + error, "error");
});
checkUpdateStatus();
}, [snapshotUpdate]);
useEffect(() => {
@@ -79,13 +75,13 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
const update = await checkUpdate();
if (update) {
const { skipVersions } = useUpdateStore.getState();
setVisible(!skipVersions.includes(update.version));
setUpdateInfo(update);
if (skipVersion === update.version) return;
setVisible(true);
}
}, [skipVersion]);
}, [skipVersions]);
const cursorClassName = useMemo(() => {
return state.loading ? "cursor-not-allowed" : "cursor-pointer";
@@ -133,7 +129,9 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
const handleSkip = () => {
if (state.loading) return;
setSkipVersion(updateInfo?.version);
const { skipVersions, updateInfo } = useUpdateStore.getState();
setSkipVersions([...skipVersions, updateInfo.version]);
isCheckPage ? hide_check() : setVisible(false);
};
@@ -182,7 +180,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
<img src={isDark ? darkIcon : lightIcon} className="h-6" />
<div className="text-[#333] text-sm leading-5 py-2 dark:text-[#D8D8D8] text-center">
{updateInfo?.available ? (
{updateInfo ? (
isOptional ? (
t("update.optional_description")
) : (
@@ -196,7 +194,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
)}
</div>
{updateInfo?.available ? (
{updateInfo ? (
<div
className="text-xs text-[#0072FF] cursor-pointer"
onClick={() =>
@@ -223,21 +221,21 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
cursorClassName,
state.loading && "opacity-50"
)}
onClick={updateInfo?.available ? handleDownload : handleSkip}
onClick={updateInfo ? handleDownload : handleSkip}
>
{state.loading ? (
<div className="flex justify-center items-center gap-2">
<LoaderCircle className="animate-spin size-5" />
{percent}%
</div>
) : updateInfo?.available ? (
) : updateInfo ? (
t("update.button.install")
) : (
t("update.button.ok")
)}
</Button>
{updateInfo?.available && isOptional && (
{!isCheckPage && updateInfo && isOptional && (
<div
className={clsx("text-xs text-[#999]", cursorClassName)}
onClick={handleSkip}

35
src/hooks/useTogglePin.ts Normal file
View File

@@ -0,0 +1,35 @@
import { useCallback } from "react";
import { useAppStore } from "@/stores/appStore";
import platformAdapter from "@/utils/platformAdapter";
import { toggle_move_to_active_space_attribute } from "@/commands/system";
import { isMac } from "@/utils/platform";
interface UseTogglePinOptions {
onPinChange?: (isPinned: boolean) => void;
}
export const useTogglePin = (options?: UseTogglePinOptions) => {
const { isPinned, setIsPinned } = useAppStore();
const togglePin = useCallback(async () => {
try {
const newPinned = !isPinned;
if (options?.onPinChange) {
options.onPinChange(newPinned);
}
await platformAdapter.setAlwaysOnTop(newPinned);
setIsPinned(newPinned);
isMac && toggle_move_to_active_space_attribute();
} catch (err) {
console.error("Failed to toggle window pin state:", err);
}
}, [isPinned, setIsPinned, options?.onPinChange]);
return {
isPinned,
togglePin,
};
};

View File

@@ -4,8 +4,8 @@ import { persist } from "zustand/middleware";
export type IUpdateStore = {
visible: boolean;
setVisible: (visible: boolean) => void;
skipVersion?: string;
setSkipVersion: (skipVersion?: string) => void;
skipVersions: string[];
setSkipVersions: (skipVersions: string[]) => void;
isOptional: boolean;
setIsOptional: (isOptional: boolean) => void;
updateInfo?: any;
@@ -19,8 +19,9 @@ export const useUpdateStore = create<IUpdateStore>()(
setVisible: (visible: boolean) => {
return set({ visible });
},
setSkipVersion: (skipVersion?: string) => {
return set({ skipVersion });
skipVersions: [],
setSkipVersions: (skipVersions: string[]) => {
return set({ skipVersions });
},
isOptional: true,
setIsOptional: (isOptional: boolean) => {
@@ -33,7 +34,7 @@ export const useUpdateStore = create<IUpdateStore>()(
{
name: "update-store",
partialize: (state) => ({
skipVersion: state.skipVersion,
skipVersions: state.skipVersions,
}),
}
)