Skip to content

Commit 97d2450

Browse files
feat: selection settings add & delete (#992)
* feat: add selection window page * fix: chat input * feat: add selection page * chore: add * chore: test * feat: add * feat: add store * feat: add selection settings * chore: remove unused code * docs: add release note * docs: add release note * chore: format code * chore: format code * fix: copy error * disable hashbrown default feature * Enable unstable feature allocator_api To make coco-app compile in CI: ``` --> /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:3856:12 | 3856 | impl<T, A: Allocator> RawIntoIter<T, A> { | ^^^^^^^^^ | = note: see issue #32838 <rust-lang/rust#32838> for more information = help: add `#![feature(allocator_api)]` to the crate attributes to enable = note: this compiler was built on 2025-06-25; consider upgrading it if it is out of date ``` I don't know why it does not compile, feature `allocator-api2` is enabled for `hashbrown 0.15.5`, so technically [1] it should not use the allocator APIs from the std. According to [2], enabling the `nightly` feature of `allocator-api2` may cause this issue as well, but it is not enabled in our case either. Anyway, enabling `#![feature(allocator_api)]` should make it work. [1]: https://github.com/rust-lang/hashbrown/blob/b751eef8e99ccf3652046ef4a9e1ec47c1bfb78d/src/raw/alloc.rs#L26-L47 [2]: rust-lang/hashbrown#564 * put it in main.rs * format main.rs * Enable default-features for hashbrown 0.15.5 * format main.rs * enable feature allocator-api2 * feat: add selection set config * fix: selection setting * fix: ci error * fix: ci error * fix: ci error * fix: ci error * merge: merge main * fix: rust code warn * fix: rust code error * fix: rust code error * fix: selection settings * style: selection styles * style: selection styles * feat: selection settings add & delete * feat: selection settings add & delete * feat: selection settings add & delete * style: selection styles * chore: add @tauri-store/zustand plugin * refactor: the selection store using @tauri-store/zustand * fix: data error * fix: data error * chore: remove config * chore: selection * chore: selection * chore: width * chore: ignore selection in the app itself * style: selection styles * style: remove * docs: add notes * chore: add permission check * chore: selection * chore: style & store --------- Co-authored-by: Steve Lau <stevelauc@outlook.com>
1 parent 18828ab commit 97d2450

27 files changed

Lines changed: 1260 additions & 437 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Information about release notes of Coco App is provided here.
1515

1616
- feat: add selection toolbar window for mac #980
1717
- feat: add a heartbeat worker to check Coco server availability #988
18+
- feat: selection settings add & delete #992
1819

1920
### 🐛 Bug fix
2021

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"dependencies": {
2121
"@headlessui/react": "^2.2.2",
22+
"@infinilabs/custom-icons": "0.0.4",
2223
"@radix-ui/react-separator": "^1.1.8",
2324
"@radix-ui/react-slot": "^1.2.3",
2425
"@tauri-apps/api": "^2.5.0",

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ async fn change_window_height(handle: AppHandle, height: u32) {
6565

6666
let outer_size = window.outer_size().unwrap();
6767
let window_width = outer_size.width as i32;
68-
let window_height = outer_size.height as i32;
6968

7069
let x = monitor_position.x + (monitor_size.width as i32 - window_width) / 2;
7170

src-tauri/src/selection_monitor.rs

Lines changed: 330 additions & 32 deletions
Large diffs are not rendered by default.

src/components/Assistant/ServerList.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export function ServerList({ clearChat }: ServerListProps) {
4646
const [isRefreshing, setIsRefreshing] = useState(false);
4747
const [highlightId, setHighlightId] = useState<string>("");
4848

49+
const targetServerId = useSearchStore((state) => {
50+
return state.targetServerId;
51+
});
52+
const setTargetServerId = useSearchStore((state) => {
53+
return state.setTargetServerId;
54+
});
4955
const askAiServerId = useSearchStore((state) => {
5056
return state.askAiServerId;
5157
});
@@ -102,17 +108,20 @@ export function ServerList({ clearChat }: ServerListProps) {
102108
}, [serverList]);
103109

104110
useEffect(() => {
105-
if (!askAiServerId || serverList.length === 0) return;
106-
107-
const matched = serverList.find((server) => {
108-
return server.id === askAiServerId;
109-
});
111+
const targetId = targetServerId ?? askAiServerId;
112+
if (!targetId || list.length === 0) return;
110113

114+
const matched = list.find((server) => server.id === targetId);
111115
if (!matched) return;
112116

113117
switchServer(matched);
114-
setAskAiServerId(void 0);
115-
}, [serverList, askAiServerId]);
118+
setHighlightId(matched.id);
119+
if (targetServerId) {
120+
setTargetServerId(void 0);
121+
} else {
122+
setAskAiServerId(void 0);
123+
}
124+
}, [list, askAiServerId, targetServerId]);
116125

117126
useEffect(() => {
118127
if (!isTauri) return;
@@ -291,4 +300,4 @@ export function ServerList({ clearChat }: ServerListProps) {
291300
</PopoverPanel>
292301
</Popover>
293302
);
294-
}
303+
}

src/components/SearchChat/index.tsx

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -316,35 +316,50 @@ function SearchChat({
316316
const { normalOpacity, blurOpacity } = useAppearanceStore();
317317

318318
useEffect(() => {
319-
const unlistenAsk = platformAdapter.listenEvent("selection-ask-ai", ({ payload }: any) => {
320-
const value = typeof payload === "string" ? payload : String(payload?.text ?? "");
321-
dispatch({ type: "SET_CHAT_MODE", payload: true });
322-
dispatch({ type: "SET_INPUT", payload: value });
323-
platformAdapter.showWindow();
324-
});
325-
326-
const unlistenAction = platformAdapter.listenEvent("selection-action", ({ payload }: any) => {
327-
const { action, text, assistantId } = payload || {};
328-
const value = String(text ?? "");
329-
if (action === "search") {
330-
dispatch({ type: "SET_CHAT_MODE", payload: false });
331-
dispatch({ type: "SET_INPUT", payload: value });
332-
const { setSearchValue } = useSearchStore.getState();
333-
setSearchValue(value);
334-
platformAdapter.showWindow();
335-
} else if (action === "chat") {
319+
const unlistenAsk = platformAdapter.listenEvent(
320+
"selection-ask-ai",
321+
({ payload }: any) => {
322+
const value =
323+
typeof payload === "string" ? payload : String(payload?.text ?? "");
336324
dispatch({ type: "SET_CHAT_MODE", payload: true });
337325
dispatch({ type: "SET_INPUT", payload: value });
326+
platformAdapter.showWindow();
327+
}
328+
);
329+
330+
const unlistenAction = platformAdapter.listenEvent(
331+
"selection-action",
332+
({ payload }: any) => {
333+
const { action, text, assistantId, serverId } = payload || {};
334+
const value = String(text ?? "");
335+
336+
//
337+
if (action === "search") {
338+
dispatch({ type: "SET_CHAT_MODE", payload: false });
339+
dispatch({ type: "SET_INPUT", payload: value });
340+
const { setSearchValue } = useSearchStore.getState();
341+
setSearchValue(value);
342+
} else if (action === "chat") {
343+
dispatch({ type: "SET_CHAT_MODE", payload: true });
344+
dispatch({ type: "SET_INPUT", payload: value });
345+
//
346+
const { setTargetServerId, setTargetAssistantId } =
347+
useSearchStore.getState();
348+
349+
if (serverId) {
350+
setTargetServerId(serverId);
351+
}
338352

339-
const { assistantList } = useConnectStore.getState();
340-
const assistant = assistantList.find((item) => item._source?.id === assistantId);
341-
if (assistant) {
342-
const { setTargetAssistantId } = useSearchStore.getState();
343-
setTargetAssistantId(assistant._id);
353+
const { assistantList } = useConnectStore.getState();
354+
const assistant = assistantList.find(
355+
(item) => item._source?.id === assistantId
356+
);
357+
if (assistant) {
358+
setTargetAssistantId(assistant._id);
359+
}
344360
}
345-
platformAdapter.showWindow();
346361
}
347-
});
362+
);
348363

349364
return () => {
350365
unlistenAsk.then((fn) => fn());
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Separator } from "@radix-ui/react-separator";
2+
3+
import cocoLogoImg from "@/assets/app-icon.png";
4+
import SelectionToolbar from "@/components/Selection/Toolbar";
5+
import type { ActionConfig, ButtonConfig } from "@/components/Settings/Advanced/components/Selection/config";
6+
7+
export default function HeaderToolbar({
8+
buttons,
9+
iconsOnly,
10+
onAction,
11+
onLogoClick,
12+
className,
13+
rootRef,
14+
children,
15+
}: {
16+
buttons: ButtonConfig[];
17+
iconsOnly: boolean;
18+
onAction: (action: ActionConfig) => void;
19+
onLogoClick?: () => void;
20+
className?: string;
21+
rootRef?: React.Ref<HTMLDivElement>;
22+
children?: React.ReactNode;
23+
}) {
24+
return (
25+
<div
26+
ref={rootRef}
27+
data-tauri-drag-region="false"
28+
className={`flex items-center gap-1 px-2 py-1 flex-nowrap overflow-hidden ${className ?? ""}`}
29+
>
30+
<img
31+
src={cocoLogoImg}
32+
alt="Coco Logo"
33+
className="w-6 h-6"
34+
onClick={onLogoClick}
35+
onError={(e) => {
36+
try {
37+
(e.target as HTMLImageElement).src = "/src-tauri/assets/logo.png";
38+
} catch {}
39+
}}
40+
/>
41+
42+
<Separator
43+
orientation="vertical"
44+
decorative
45+
className="mx-1 h-4 w-px bg-gray-300 dark:bg-white/30 shrink-0"
46+
/>
47+
48+
<SelectionToolbar
49+
buttons={buttons}
50+
iconsOnly={iconsOnly}
51+
onAction={onAction}
52+
requireAssistantCheck={false}
53+
/>
54+
55+
{children}
56+
</div>
57+
);
58+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import clsx from "clsx";
2+
import { useTranslation } from "react-i18next";
3+
import { Search } from "lucide-react";
4+
5+
import {
6+
ActionConfig,
7+
ButtonConfig,
8+
IconConfig,
9+
resolveLucideIcon,
10+
} from "@/components/Settings/Advanced/components/Selection/config";
11+
12+
const requiresAssistant = (type?: string) =>
13+
type === "ask_ai" || type === "translate" || type === "summary";
14+
15+
function IconRenderer({ icon }: { icon?: IconConfig }) {
16+
if (icon?.type === "lucide") {
17+
const Comp = resolveLucideIcon(icon?.name);
18+
if (Comp) {
19+
return (
20+
<Comp
21+
className="size-4 transition-transform duration-150"
22+
// style={icon?.color ? { color: icon.color } : undefined}
23+
/>
24+
);
25+
}
26+
return (
27+
<Search
28+
className="size-4 transition-transform duration-150"
29+
// style={icon?.color ? { color: icon.color } : undefined}
30+
/>
31+
);
32+
}
33+
if (icon?.type === "custom" && icon?.dataUrl) {
34+
return (
35+
<img
36+
src={icon.dataUrl}
37+
className="size-4 rounded"
38+
alt=""
39+
// style={
40+
// icon?.color
41+
// ? { filter: `drop-shadow(0 0 0 ${icon.color})` }
42+
// : undefined
43+
// }
44+
/>
45+
);
46+
}
47+
return <Search className="size-4 text-[#6366F1]" />;
48+
}
49+
50+
function ToolbarButton({
51+
btn,
52+
onClick,
53+
showLabel,
54+
}: {
55+
btn: ButtonConfig;
56+
onClick: () => void;
57+
showLabel: boolean;
58+
}) {
59+
const { t } = useTranslation();
60+
const label = btn?.labelKey ? t(btn.labelKey) : btn?.label || btn?.id || "";
61+
return (
62+
<button
63+
className="flex items-center gap-1 p-1 rounded-md cursor-pointer whitespace-nowrap transition-all duration-150"
64+
onClick={onClick}
65+
title={label}
66+
>
67+
<IconRenderer icon={btn?.icon} />
68+
{showLabel && (
69+
<span className="text-[12px] transition-opacity duration-150">
70+
{label}
71+
</span>
72+
)}
73+
</button>
74+
);
75+
}
76+
77+
export default function SelectionToolbar({
78+
buttons,
79+
iconsOnly,
80+
onAction,
81+
className,
82+
requireAssistantCheck = true,
83+
}: {
84+
buttons: ButtonConfig[];
85+
iconsOnly: boolean;
86+
onAction: (action: ActionConfig) => void;
87+
className?: string;
88+
requireAssistantCheck?: boolean;
89+
}) {
90+
const visibleButtons = (Array.isArray(buttons) ? buttons : []).filter((btn: any) => {
91+
if (!requireAssistantCheck) return true;
92+
const type = btn?.action?.type;
93+
if (requiresAssistant(type)) {
94+
return Boolean(btn?.action?.assistantId);
95+
}
96+
return true;
97+
});
98+
99+
return (
100+
<div
101+
className={clsx(
102+
"flex items-center gap-1 flex-nowrap overflow-hidden",
103+
className
104+
)}
105+
>
106+
{visibleButtons.map((btn) => (
107+
<ToolbarButton
108+
key={btn.id}
109+
btn={btn}
110+
onClick={() => onAction(btn.action)}
111+
showLabel={!iconsOnly}
112+
/>
113+
))}
114+
</div>
115+
);
116+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from "react";
2+
import { useTranslation } from "react-i18next";
3+
import { Plus } from "lucide-react";
4+
5+
import { Button } from "@/components/ui/button";
6+
import { ButtonConfig } from "./config";
7+
import AddChatDialog from "./AddChatDialog";
8+
9+
interface AddChatButtonProps {
10+
serverList: any[];
11+
onAdd: (btn: ButtonConfig) => void;
12+
}
13+
14+
export function AddChatButton({ serverList, onAdd }: AddChatButtonProps) {
15+
const { t } = useTranslation();
16+
const [open, setOpen] = useState(false);
17+
18+
return (
19+
<div className="pt-1">
20+
<Button
21+
variant="ghost"
22+
className="inline-flex items-center gap-2 border border-dashed border-border hover:border-primary/50 hover:bg-secondary/50 text-muted-foreground transition-all duration-200"
23+
onClick={() => setOpen(true)}
24+
>
25+
<Plus className="w-4 h-4" />
26+
{t("selection.actions.addChat")}
27+
</Button>
28+
29+
<AddChatDialog
30+
serverList={serverList}
31+
open={open}
32+
onOpenChange={setOpen}
33+
onAdd={onAdd}
34+
/>
35+
</div>
36+
);
37+
}

0 commit comments

Comments
 (0)