cue-console 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app/page.tsx +91 -1
- package/src/components/chat/avatar-picker-dialog.tsx +17 -2
- package/src/components/chat/chat-header.tsx +29 -1
- package/src/components/chat/image-preview-dialog.tsx +6 -2
- package/src/components/chat/message-bubble.tsx +9 -1
- package/src/components/chat/user-response-bubble.tsx +6 -2
- package/src/components/chat-composer.tsx +5 -3
- package/src/components/chat-view.tsx +80 -31
- package/src/components/conversation-list.tsx +42 -28
- package/src/components/markdown-renderer.tsx +12 -1
- package/src/hooks/use-avatar.ts +0 -1
- package/src/hooks/use-conversation-timeline.ts +0 -1
- package/src/hooks/use-mentions.ts +0 -1
- package/src/hooks/use-message-queue.ts +0 -1
- package/src/lib/actions.ts +43 -0
- package/src/lib/avatar.ts +0 -1
- package/src/lib/db.ts +95 -0
- package/src/lib/types.ts +2 -0
- package/src/types/chat.ts +1 -1
package/package.json
CHANGED
package/src/app/page.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { ConversationList } from "@/components/conversation-list";
|
|
|
5
5
|
import { ChatView } from "@/components/chat-view";
|
|
6
6
|
import { CreateGroupDialog } from "@/components/create-group-dialog";
|
|
7
7
|
import { MessageCircle } from "lucide-react";
|
|
8
|
-
import { claimWorkerLease, processQueueTick } from "@/lib/actions";
|
|
8
|
+
import { claimWorkerLease, fetchBotEnabledConversations, processBotTick, processQueueTick } from "@/lib/actions";
|
|
9
9
|
|
|
10
10
|
export default function Home() {
|
|
11
11
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
@@ -114,6 +114,96 @@ export default function Home() {
|
|
|
114
114
|
};
|
|
115
115
|
}, []);
|
|
116
116
|
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
let stopped = false;
|
|
119
|
+
const holderId =
|
|
120
|
+
(globalThis.crypto && "randomUUID" in globalThis.crypto
|
|
121
|
+
? (globalThis.crypto as Crypto).randomUUID()
|
|
122
|
+
: `${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
123
|
+
const leaseKey = "cue-console:global-bot-worker";
|
|
124
|
+
const leaseTtlMs = 12_000;
|
|
125
|
+
const claimEveryMs = 4_000;
|
|
126
|
+
const tickEveryMs = 2_500;
|
|
127
|
+
|
|
128
|
+
let isLeader = false;
|
|
129
|
+
let tickTimer: ReturnType<typeof setInterval> | null = null;
|
|
130
|
+
let claimTimer: ReturnType<typeof setInterval> | null = null;
|
|
131
|
+
let rrIndex = 0;
|
|
132
|
+
|
|
133
|
+
const stopTick = () => {
|
|
134
|
+
if (tickTimer) {
|
|
135
|
+
clearInterval(tickTimer);
|
|
136
|
+
tickTimer = null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const startTick = () => {
|
|
141
|
+
if (tickTimer) return;
|
|
142
|
+
tickTimer = setInterval(() => {
|
|
143
|
+
if (document.visibilityState !== "visible") return;
|
|
144
|
+
void (async () => {
|
|
145
|
+
try {
|
|
146
|
+
const enabled = await fetchBotEnabledConversations(200);
|
|
147
|
+
if (!Array.isArray(enabled) || enabled.length === 0) return;
|
|
148
|
+
|
|
149
|
+
const maxPerTick = 10;
|
|
150
|
+
const n = Math.min(enabled.length, maxPerTick);
|
|
151
|
+
for (let i = 0; i < n; i += 1) {
|
|
152
|
+
const idx = (rrIndex + i) % enabled.length;
|
|
153
|
+
const row = enabled[idx] as { conv_type?: unknown; conv_id?: unknown };
|
|
154
|
+
const convType = row?.conv_type === "group" ? "group" : "agent";
|
|
155
|
+
const convId = String(row?.conv_id || "").trim();
|
|
156
|
+
if (!convId) continue;
|
|
157
|
+
await processBotTick({ holderId, convType, convId, limit: 80 });
|
|
158
|
+
}
|
|
159
|
+
rrIndex = (rrIndex + n) % enabled.length;
|
|
160
|
+
} catch {
|
|
161
|
+
// ignore
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
}, tickEveryMs);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const claimOnce = async () => {
|
|
168
|
+
try {
|
|
169
|
+
const res = await claimWorkerLease({ leaseKey, holderId, ttlMs: leaseTtlMs });
|
|
170
|
+
const nextLeader = Boolean(res.acquired && res.holderId === holderId);
|
|
171
|
+
if (nextLeader !== isLeader) {
|
|
172
|
+
isLeader = nextLeader;
|
|
173
|
+
if (isLeader) startTick();
|
|
174
|
+
else stopTick();
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// ignore
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const boot = async () => {
|
|
182
|
+
await claimOnce();
|
|
183
|
+
if (stopped) return;
|
|
184
|
+
claimTimer = setInterval(() => {
|
|
185
|
+
if (document.visibilityState !== "visible") return;
|
|
186
|
+
void claimOnce();
|
|
187
|
+
}, claimEveryMs);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
void boot();
|
|
191
|
+
|
|
192
|
+
const onVisibilityChange = () => {
|
|
193
|
+
if (document.visibilityState === "visible") {
|
|
194
|
+
void claimOnce();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
198
|
+
|
|
199
|
+
return () => {
|
|
200
|
+
stopped = true;
|
|
201
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
202
|
+
stopTick();
|
|
203
|
+
if (claimTimer) clearInterval(claimTimer);
|
|
204
|
+
};
|
|
205
|
+
}, []);
|
|
206
|
+
|
|
117
207
|
useEffect(() => {
|
|
118
208
|
try {
|
|
119
209
|
window.localStorage.setItem("cuehub.sidebarCollapsed", sidebarCollapsed ? "1" : "0");
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DialogTitle,
|
|
9
9
|
} from "@/components/ui/dialog";
|
|
10
10
|
import { getAgentEmoji } from "@/lib/utils";
|
|
11
|
+
import Image from "next/image";
|
|
11
12
|
|
|
12
13
|
interface AvatarPickerDialogProps {
|
|
13
14
|
open: boolean;
|
|
@@ -41,7 +42,14 @@ export function AvatarPickerDialog({
|
|
|
41
42
|
<div className="flex items-center gap-3">
|
|
42
43
|
<div className="h-14 w-14 rounded-full bg-muted overflow-hidden">
|
|
43
44
|
{currentAvatarUrl ? (
|
|
44
|
-
<
|
|
45
|
+
<Image
|
|
46
|
+
src={currentAvatarUrl}
|
|
47
|
+
alt=""
|
|
48
|
+
width={56}
|
|
49
|
+
height={56}
|
|
50
|
+
unoptimized
|
|
51
|
+
className="h-full w-full"
|
|
52
|
+
/>
|
|
45
53
|
) : (
|
|
46
54
|
<div className="flex h-full w-full items-center justify-center text-xl">
|
|
47
55
|
{target.kind === "group" ? "👥" : getAgentEmoji(target.id)}
|
|
@@ -70,7 +78,14 @@ export function AvatarPickerDialog({
|
|
|
70
78
|
title="Apply"
|
|
71
79
|
>
|
|
72
80
|
{c.url ? (
|
|
73
|
-
<
|
|
81
|
+
<Image
|
|
82
|
+
src={c.url}
|
|
83
|
+
alt=""
|
|
84
|
+
width={48}
|
|
85
|
+
height={48}
|
|
86
|
+
unoptimized
|
|
87
|
+
className="h-full w-full"
|
|
88
|
+
/>
|
|
74
89
|
) : null}
|
|
75
90
|
</button>
|
|
76
91
|
))}
|
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { Button } from "@/components/ui/button";
|
|
5
5
|
import { cn, getAgentEmoji } from "@/lib/utils";
|
|
6
6
|
import { ChevronLeft, Github } from "lucide-react";
|
|
7
|
+
import Image from "next/image";
|
|
7
8
|
|
|
8
9
|
interface ChatHeaderProps {
|
|
9
10
|
type: "agent" | "group";
|
|
@@ -11,6 +12,8 @@ interface ChatHeaderProps {
|
|
|
11
12
|
titleDisplay: string;
|
|
12
13
|
avatarUrl: string;
|
|
13
14
|
members: string[];
|
|
15
|
+
agentRuntime?: string;
|
|
16
|
+
projectName?: string;
|
|
14
17
|
onBack?: () => void;
|
|
15
18
|
onAvatarClick: () => void;
|
|
16
19
|
onTitleChange: (newTitle: string) => Promise<void>;
|
|
@@ -22,6 +25,8 @@ export function ChatHeader({
|
|
|
22
25
|
titleDisplay,
|
|
23
26
|
avatarUrl,
|
|
24
27
|
members,
|
|
28
|
+
agentRuntime,
|
|
29
|
+
projectName,
|
|
25
30
|
onBack,
|
|
26
31
|
onAvatarClick,
|
|
27
32
|
onTitleChange,
|
|
@@ -29,6 +34,8 @@ export function ChatHeader({
|
|
|
29
34
|
const [editingTitle, setEditingTitle] = useState(false);
|
|
30
35
|
const [titleDraft, setTitleDraft] = useState("");
|
|
31
36
|
|
|
37
|
+
const showAgentTags = type === "agent" && (agentRuntime || projectName);
|
|
38
|
+
|
|
32
39
|
const beginEditTitle = () => {
|
|
33
40
|
setEditingTitle(true);
|
|
34
41
|
setTitleDraft(titleDisplay);
|
|
@@ -65,7 +72,14 @@ export function ChatHeader({
|
|
|
65
72
|
title="Change avatar"
|
|
66
73
|
>
|
|
67
74
|
{avatarUrl ? (
|
|
68
|
-
<
|
|
75
|
+
<Image
|
|
76
|
+
src={avatarUrl}
|
|
77
|
+
alt=""
|
|
78
|
+
width={36}
|
|
79
|
+
height={36}
|
|
80
|
+
unoptimized
|
|
81
|
+
className="h-full w-full"
|
|
82
|
+
/>
|
|
69
83
|
) : (
|
|
70
84
|
<span className="flex h-full w-full items-center justify-center text-lg">
|
|
71
85
|
{type === "group" ? "👥" : getAgentEmoji(id)}
|
|
@@ -102,6 +116,20 @@ export function ChatHeader({
|
|
|
102
116
|
{titleDisplay}
|
|
103
117
|
</h2>
|
|
104
118
|
)}
|
|
119
|
+
{showAgentTags && (
|
|
120
|
+
<div className="mt-0.5 flex flex-wrap gap-1">
|
|
121
|
+
{agentRuntime && (
|
|
122
|
+
<span className="inline-flex items-center rounded-full border bg-white/55 px-2 py-0.5 text-[10px] text-muted-foreground">
|
|
123
|
+
{agentRuntime}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
{projectName && (
|
|
127
|
+
<span className="inline-flex items-center rounded-full border bg-white/55 px-2 py-0.5 text-[10px] text-muted-foreground">
|
|
128
|
+
{projectName}
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
105
133
|
{type === "group" && members.length > 0 && (
|
|
106
134
|
<p className="text-xs text-muted-foreground truncate">
|
|
107
135
|
{members.length} member{members.length === 1 ? "" : "s"}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DialogHeader,
|
|
7
7
|
DialogTitle,
|
|
8
8
|
} from "@/components/ui/dialog";
|
|
9
|
+
import Image from "next/image";
|
|
9
10
|
|
|
10
11
|
interface ImagePreviewDialogProps {
|
|
11
12
|
image: { mime_type: string; base64_data: string } | null;
|
|
@@ -21,10 +22,13 @@ export function ImagePreviewDialog({ image, onClose }: ImagePreviewDialogProps)
|
|
|
21
22
|
</DialogHeader>
|
|
22
23
|
{image && (
|
|
23
24
|
<div className="flex items-center justify-center">
|
|
24
|
-
<
|
|
25
|
+
<Image
|
|
25
26
|
src={`data:${image.mime_type};base64,${image.base64_data}`}
|
|
26
27
|
alt=""
|
|
27
|
-
|
|
28
|
+
width={1200}
|
|
29
|
+
height={800}
|
|
30
|
+
unoptimized
|
|
31
|
+
className="max-h-[70vh] h-auto w-auto rounded-lg"
|
|
28
32
|
/>
|
|
29
33
|
</div>
|
|
30
34
|
)}
|
|
@@ -6,6 +6,7 @@ import { MarkdownRenderer } from "@/components/markdown-renderer";
|
|
|
6
6
|
import { PayloadCard } from "@/components/payload-card";
|
|
7
7
|
import type { CueRequest } from "@/lib/actions";
|
|
8
8
|
import { Copy, Check } from "lucide-react";
|
|
9
|
+
import Image from "next/image";
|
|
9
10
|
|
|
10
11
|
interface MessageBubbleProps {
|
|
11
12
|
request: CueRequest;
|
|
@@ -110,7 +111,14 @@ export function MessageBubble({
|
|
|
110
111
|
}}
|
|
111
112
|
>
|
|
112
113
|
{avatarUrl ? (
|
|
113
|
-
<
|
|
114
|
+
<Image
|
|
115
|
+
src={avatarUrl}
|
|
116
|
+
alt=""
|
|
117
|
+
width={36}
|
|
118
|
+
height={36}
|
|
119
|
+
unoptimized
|
|
120
|
+
className="h-full w-full rounded-full"
|
|
121
|
+
/>
|
|
114
122
|
) : (
|
|
115
123
|
getAgentEmoji(request.agent_id || "")
|
|
116
124
|
)}
|
|
@@ -3,6 +3,7 @@ import { cn, formatFullTime } from "@/lib/utils";
|
|
|
3
3
|
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
|
4
4
|
import type { CueResponse } from "@/lib/actions";
|
|
5
5
|
import { useConfig } from "@/contexts/config-context";
|
|
6
|
+
import Image from "next/image";
|
|
6
7
|
|
|
7
8
|
interface UserResponseBubbleProps {
|
|
8
9
|
response: CueResponse;
|
|
@@ -143,11 +144,14 @@ export function UserResponseBubble({
|
|
|
143
144
|
const b64 = String(obj?.inline_base64 || "");
|
|
144
145
|
const img = { mime_type: mime, base64_data: b64 };
|
|
145
146
|
return (
|
|
146
|
-
<
|
|
147
|
+
<Image
|
|
147
148
|
key={i}
|
|
148
149
|
src={`data:${img.mime_type};base64,${img.base64_data}`}
|
|
149
150
|
alt=""
|
|
150
|
-
|
|
151
|
+
width={512}
|
|
152
|
+
height={256}
|
|
153
|
+
unoptimized
|
|
154
|
+
className="max-h-32 max-w-full h-auto w-auto rounded cursor-pointer"
|
|
151
155
|
onClick={() => onPreview?.(img)}
|
|
152
156
|
/>
|
|
153
157
|
);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { cn, getAgentEmoji } from "@/lib/utils";
|
|
22
22
|
import { setAgentDisplayName } from "@/lib/actions";
|
|
23
23
|
import { Bot, CornerUpLeft, GripVertical, Plus, Send, Trash2, X } from "lucide-react";
|
|
24
|
+
import Image from "next/image";
|
|
24
25
|
|
|
25
26
|
type MentionDraft = {
|
|
26
27
|
userId: string;
|
|
@@ -75,7 +76,6 @@ export function ChatComposer({
|
|
|
75
76
|
setInput,
|
|
76
77
|
images,
|
|
77
78
|
setImages,
|
|
78
|
-
setNotice,
|
|
79
79
|
setPreviewImage,
|
|
80
80
|
botEnabled,
|
|
81
81
|
onToggleBot,
|
|
@@ -119,7 +119,6 @@ export function ChatComposer({
|
|
|
119
119
|
setInput: Dispatch<SetStateAction<string>>;
|
|
120
120
|
images: { mime_type: string; base64_data: string; file_name?: string }[];
|
|
121
121
|
setImages: Dispatch<SetStateAction<{ mime_type: string; base64_data: string; file_name?: string }[]>>;
|
|
122
|
-
setNotice: Dispatch<SetStateAction<string | null>>;
|
|
123
122
|
setPreviewImage: Dispatch<SetStateAction<{ mime_type: string; base64_data: string } | null>>;
|
|
124
123
|
botEnabled: boolean;
|
|
125
124
|
onToggleBot: () => Promise<boolean>;
|
|
@@ -317,9 +316,12 @@ export function ChatComposer({
|
|
|
317
316
|
{images.map((img, i) => (
|
|
318
317
|
<div key={i} className="relative shrink-0">
|
|
319
318
|
{img.mime_type.startsWith("image/") ? (
|
|
320
|
-
<
|
|
319
|
+
<Image
|
|
321
320
|
src={`data:${img.mime_type};base64,${img.base64_data}`}
|
|
322
321
|
alt=""
|
|
322
|
+
width={64}
|
|
323
|
+
height={64}
|
|
324
|
+
unoptimized
|
|
323
325
|
className="h-16 w-16 rounded-xl object-cover shadow-sm ring-1 ring-border/60 cursor-pointer"
|
|
324
326
|
onClick={() => setPreviewImage(img)}
|
|
325
327
|
/>
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
useMemo,
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
|
-
type ClipboardEvent,
|
|
11
10
|
} from "react";
|
|
12
11
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
13
12
|
import { Button } from "@/components/ui/button";
|
|
@@ -19,14 +18,16 @@ import {
|
|
|
19
18
|
} from "@/components/ui/dialog";
|
|
20
19
|
import { cn, getAgentEmoji } from "@/lib/utils";
|
|
21
20
|
import { randomSeed } from "@/lib/avatar";
|
|
21
|
+
import Image from "next/image";
|
|
22
22
|
import {
|
|
23
23
|
setAgentDisplayName,
|
|
24
24
|
setGroupName,
|
|
25
25
|
submitResponse,
|
|
26
26
|
cancelRequest,
|
|
27
|
-
batchRespond,
|
|
28
27
|
processBotTick,
|
|
29
|
-
|
|
28
|
+
fetchBotEnabled,
|
|
29
|
+
updateBotEnabled,
|
|
30
|
+
fetchAgentEnv,
|
|
30
31
|
} from "@/lib/actions";
|
|
31
32
|
import { ChatComposer } from "@/components/chat-composer";
|
|
32
33
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
@@ -45,7 +46,7 @@ import { useMessageSender } from "@/hooks/use-message-sender";
|
|
|
45
46
|
import { useFileHandler } from "@/hooks/use-file-handler";
|
|
46
47
|
import { useDraftPersistence } from "@/hooks/use-draft-persistence";
|
|
47
48
|
import { isPauseRequest, filterPendingRequests } from "@/lib/chat-logic";
|
|
48
|
-
import type { ChatType
|
|
49
|
+
import type { ChatType } from "@/types/chat";
|
|
49
50
|
import { ArrowDown } from "lucide-react";
|
|
50
51
|
|
|
51
52
|
function perfEnabled(): boolean {
|
|
@@ -78,10 +79,6 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
78
79
|
const deferredInput = useDeferredValue(input);
|
|
79
80
|
const imagesRef = useRef(images);
|
|
80
81
|
|
|
81
|
-
const botStorageKey = useMemo(() => {
|
|
82
|
-
return `cue-console:botEnabled:${type}:${id}`;
|
|
83
|
-
}, [type, id]);
|
|
84
|
-
|
|
85
82
|
const [botEnabled, setBotEnabled] = useState(false);
|
|
86
83
|
|
|
87
84
|
const { soundEnabled, setSoundEnabled, playDing } = useAudioNotification();
|
|
@@ -105,6 +102,8 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
105
102
|
const [members, setMembers] = useState<string[]>([]);
|
|
106
103
|
const [agentNameMap, setAgentNameMap] = useState<Record<string, string>>({});
|
|
107
104
|
const [groupTitle, setGroupTitle] = useState<string>(name);
|
|
105
|
+
const [agentRuntime, setAgentRuntime] = useState<string | undefined>(undefined);
|
|
106
|
+
const [projectName, setProjectName] = useState<string | undefined>(undefined);
|
|
108
107
|
const [previewImage, setPreviewImage] = useState<{ mime_type: string; base64_data: string } | null>(null);
|
|
109
108
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
110
109
|
|
|
@@ -161,12 +160,11 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
161
160
|
|
|
162
161
|
const {
|
|
163
162
|
queue,
|
|
164
|
-
|
|
163
|
+
setQueue,
|
|
165
164
|
enqueueCurrent,
|
|
166
165
|
removeQueued,
|
|
167
166
|
recallQueued,
|
|
168
167
|
reorderQueue,
|
|
169
|
-
setQueue,
|
|
170
168
|
} = useMessageQueue({
|
|
171
169
|
type,
|
|
172
170
|
id,
|
|
@@ -196,6 +194,32 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
196
194
|
return groupTitle;
|
|
197
195
|
}, [agentNameMap, groupTitle, id, type]);
|
|
198
196
|
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (type !== "agent") {
|
|
199
|
+
setAgentRuntime(undefined);
|
|
200
|
+
setProjectName(undefined);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let cancelled = false;
|
|
205
|
+
void (async () => {
|
|
206
|
+
try {
|
|
207
|
+
const env = await fetchAgentEnv(id);
|
|
208
|
+
if (cancelled) return;
|
|
209
|
+
setAgentRuntime(env.agentRuntime);
|
|
210
|
+
setProjectName(env.projectName);
|
|
211
|
+
} catch {
|
|
212
|
+
if (cancelled) return;
|
|
213
|
+
setAgentRuntime(undefined);
|
|
214
|
+
setProjectName(undefined);
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
return () => {
|
|
219
|
+
cancelled = true;
|
|
220
|
+
};
|
|
221
|
+
}, [id, type]);
|
|
222
|
+
|
|
199
223
|
useEffect(() => {
|
|
200
224
|
if (type !== "group") return;
|
|
201
225
|
queueMicrotask(() => setGroupTitle(name));
|
|
@@ -260,9 +284,9 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
260
284
|
const toggleBot = useCallback(async (): Promise<boolean> => {
|
|
261
285
|
const prev = botEnabled;
|
|
262
286
|
const next = !prev;
|
|
263
|
-
setBotEnabled(next);
|
|
264
287
|
try {
|
|
265
|
-
|
|
288
|
+
await updateBotEnabled(type, id, next);
|
|
289
|
+
setBotEnabled(next);
|
|
266
290
|
if (next) void triggerBotTickOnce();
|
|
267
291
|
return next;
|
|
268
292
|
} catch {
|
|
@@ -270,16 +294,24 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
270
294
|
setNotice("Failed to toggle bot");
|
|
271
295
|
return prev;
|
|
272
296
|
}
|
|
273
|
-
}, [botEnabled,
|
|
297
|
+
}, [botEnabled, id, setNotice, triggerBotTickOnce, type]);
|
|
274
298
|
|
|
275
299
|
useEffect(() => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
300
|
+
let cancelled = false;
|
|
301
|
+
void (async () => {
|
|
302
|
+
try {
|
|
303
|
+
const res = await fetchBotEnabled(type, id);
|
|
304
|
+
if (cancelled) return;
|
|
305
|
+
setBotEnabled(Boolean(res.enabled));
|
|
306
|
+
} catch {
|
|
307
|
+
if (cancelled) return;
|
|
308
|
+
setBotEnabled(false);
|
|
309
|
+
}
|
|
310
|
+
})();
|
|
311
|
+
return () => {
|
|
312
|
+
cancelled = true;
|
|
313
|
+
};
|
|
314
|
+
}, [id, type]);
|
|
283
315
|
|
|
284
316
|
useEffect(() => {
|
|
285
317
|
if (!botEnabled) return;
|
|
@@ -371,7 +403,6 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
371
403
|
await ensureAvatarUrl("agent", id);
|
|
372
404
|
if (t0) {
|
|
373
405
|
const t1 = performance.now();
|
|
374
|
-
// eslint-disable-next-line no-console
|
|
375
406
|
console.log(`[perf] ensureAvatarUrl(agent) id=${id} ${(t1 - t0).toFixed(1)}ms`);
|
|
376
407
|
}
|
|
377
408
|
})();
|
|
@@ -384,7 +415,6 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
384
415
|
await ensureAvatarUrl("group", id);
|
|
385
416
|
if (t0) {
|
|
386
417
|
const t1 = performance.now();
|
|
387
|
-
// eslint-disable-next-line no-console
|
|
388
418
|
console.log(`[perf] ensureAvatarUrl(group) id=${id} ${(t1 - t0).toFixed(1)}ms`);
|
|
389
419
|
}
|
|
390
420
|
})();
|
|
@@ -399,7 +429,6 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
399
429
|
}
|
|
400
430
|
if (t0) {
|
|
401
431
|
const t1 = performance.now();
|
|
402
|
-
// eslint-disable-next-line no-console
|
|
403
432
|
console.log(`[perf] ensureAvatarUrl(group members) group=${id} n=${members.length} ${(t1 - t0).toFixed(1)}ms`);
|
|
404
433
|
}
|
|
405
434
|
})();
|
|
@@ -485,7 +514,7 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
485
514
|
setImages([]);
|
|
486
515
|
imagesRef.current = [];
|
|
487
516
|
setMentions([]);
|
|
488
|
-
}, [type, id]);
|
|
517
|
+
}, [type, id, setBusy, setError, setImages, setInput, setMentions, setNotice]);
|
|
489
518
|
|
|
490
519
|
const loadMore = useCallback(async () => {
|
|
491
520
|
if (!nextCursor) return;
|
|
@@ -528,7 +557,7 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
528
557
|
el.addEventListener("scroll", onScroll, { passive: true });
|
|
529
558
|
onScroll();
|
|
530
559
|
return () => el.removeEventListener("scroll", onScroll);
|
|
531
|
-
}, []);
|
|
560
|
+
}, [loadMore]);
|
|
532
561
|
|
|
533
562
|
useEffect(() => {
|
|
534
563
|
const el = scrollRef.current;
|
|
@@ -619,7 +648,7 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
619
648
|
if (!notice) return;
|
|
620
649
|
const t = setTimeout(() => setNotice(null), 2200);
|
|
621
650
|
return () => clearTimeout(t);
|
|
622
|
-
}, [notice]);
|
|
651
|
+
}, [notice, setNotice]);
|
|
623
652
|
|
|
624
653
|
useEffect(() => {
|
|
625
654
|
const el = textareaRef.current;
|
|
@@ -674,6 +703,8 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
674
703
|
titleDisplay={titleDisplay}
|
|
675
704
|
avatarUrl={type === "group" ? avatarUrlMap[`group:${id}`] : avatarUrlMap[`agent:${id}`]}
|
|
676
705
|
members={members}
|
|
706
|
+
agentRuntime={agentRuntime}
|
|
707
|
+
projectName={projectName}
|
|
677
708
|
onBack={onBack}
|
|
678
709
|
onAvatarClick={() => openAvatarPicker({ kind: type, id })}
|
|
679
710
|
onTitleChange={handleTitleChange}
|
|
@@ -778,7 +809,6 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
778
809
|
setInput={setInput}
|
|
779
810
|
images={images}
|
|
780
811
|
setImages={setImages}
|
|
781
|
-
setNotice={setNotice}
|
|
782
812
|
setPreviewImage={setPreviewImage}
|
|
783
813
|
botEnabled={botEnabled}
|
|
784
814
|
onToggleBot={toggleBot}
|
|
@@ -820,10 +850,13 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
820
850
|
{previewImage ? (
|
|
821
851
|
<div className="flex items-center justify-center">
|
|
822
852
|
{((img) => (
|
|
823
|
-
<
|
|
853
|
+
<Image
|
|
824
854
|
src={`data:${img.mime_type};base64,${img.base64_data}`}
|
|
825
855
|
alt=""
|
|
826
|
-
|
|
856
|
+
width={1200}
|
|
857
|
+
height={800}
|
|
858
|
+
unoptimized
|
|
859
|
+
className="max-h-[70vh] h-auto w-auto rounded-lg"
|
|
827
860
|
/>
|
|
828
861
|
))(previewImage!)}
|
|
829
862
|
</div>
|
|
@@ -844,7 +877,14 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
844
877
|
return (
|
|
845
878
|
<div className="h-14 w-14 rounded-full bg-muted overflow-hidden">
|
|
846
879
|
{avatarUrlMap[key] ? (
|
|
847
|
-
<
|
|
880
|
+
<Image
|
|
881
|
+
src={avatarUrlMap[key]}
|
|
882
|
+
alt=""
|
|
883
|
+
width={56}
|
|
884
|
+
height={56}
|
|
885
|
+
unoptimized
|
|
886
|
+
className="h-full w-full"
|
|
887
|
+
/>
|
|
848
888
|
) : (
|
|
849
889
|
<div className="flex h-full w-full items-center justify-center text-xl">
|
|
850
890
|
{target.kind === "group" ? "👥" : getAgentEmoji(id)}
|
|
@@ -890,7 +930,16 @@ function ChatViewContent({ type, id, name, onBack }: ChatViewProps) {
|
|
|
890
930
|
}}
|
|
891
931
|
title="Apply"
|
|
892
932
|
>
|
|
893
|
-
{c.url ?
|
|
933
|
+
{c.url ? (
|
|
934
|
+
<Image
|
|
935
|
+
src={c.url}
|
|
936
|
+
alt=""
|
|
937
|
+
width={48}
|
|
938
|
+
height={48}
|
|
939
|
+
unoptimized
|
|
940
|
+
className="h-full w-full"
|
|
941
|
+
/>
|
|
942
|
+
) : null}
|
|
894
943
|
</button>
|
|
895
944
|
))}
|
|
896
945
|
</div>
|
|
@@ -38,19 +38,20 @@ import {
|
|
|
38
38
|
} from "@/lib/actions";
|
|
39
39
|
import {
|
|
40
40
|
Archive,
|
|
41
|
-
Plus,
|
|
42
|
-
MessageCircle,
|
|
43
|
-
Search,
|
|
44
|
-
ChevronDown,
|
|
45
|
-
Users,
|
|
46
41
|
Bot,
|
|
42
|
+
ChevronDown,
|
|
47
43
|
ChevronLeft,
|
|
48
44
|
ChevronRight,
|
|
45
|
+
MessageCircle,
|
|
49
46
|
MoreHorizontal,
|
|
50
47
|
Pin,
|
|
48
|
+
Plus,
|
|
49
|
+
Search,
|
|
51
50
|
Settings,
|
|
51
|
+
Users,
|
|
52
52
|
X,
|
|
53
53
|
} from "lucide-react";
|
|
54
|
+
import Image from "next/image";
|
|
54
55
|
import { ConfirmDialog } from "@/components/confirm-dialog";
|
|
55
56
|
|
|
56
57
|
function perfEnabled(): boolean {
|
|
@@ -135,8 +136,7 @@ export function ConversationList({
|
|
|
135
136
|
setPendingRequestTimeoutMs(String(cfg.pending_request_timeout_ms ?? 600000));
|
|
136
137
|
setBotModeReplyText(
|
|
137
138
|
String(
|
|
138
|
-
|
|
139
|
-
DEFAULT_USER_CONFIG.bot_mode_reply_text
|
|
139
|
+
cfg.bot_mode_reply_text || DEFAULT_USER_CONFIG.bot_mode_reply_text
|
|
140
140
|
)
|
|
141
141
|
);
|
|
142
142
|
try {
|
|
@@ -175,7 +175,6 @@ export function ConversationList({
|
|
|
175
175
|
setAvatarUrlMap((prev) => ({ ...prev, [key]: url }));
|
|
176
176
|
if (t0) {
|
|
177
177
|
const t1 = performance.now();
|
|
178
|
-
// eslint-disable-next-line no-console
|
|
179
178
|
console.log(`[perf] ensureAvatarUrl kind=${kind} id=${rawId} ${(t1 - t0).toFixed(1)}ms`);
|
|
180
179
|
}
|
|
181
180
|
} catch {
|
|
@@ -254,7 +253,6 @@ export function ConversationList({
|
|
|
254
253
|
setArchivedCount(count);
|
|
255
254
|
if (t0) {
|
|
256
255
|
const t1 = performance.now();
|
|
257
|
-
// eslint-disable-next-line no-console
|
|
258
256
|
console.log(`[perf] conversationList loadData view=${view} items=${data.length} ${(t1 - t0).toFixed(1)}ms`);
|
|
259
257
|
}
|
|
260
258
|
}, [view]);
|
|
@@ -428,7 +426,6 @@ export function ConversationList({
|
|
|
428
426
|
}, [agentsAll, pinnedKeys, pinnedSet]);
|
|
429
427
|
|
|
430
428
|
const groupsPendingTotal = groups.reduce((sum, g) => sum + g.pendingCount, 0);
|
|
431
|
-
const agentsPendingTotal = agents.reduce((sum, a) => sum + a.pendingCount, 0);
|
|
432
429
|
|
|
433
430
|
const isCollapsed = !!collapsed;
|
|
434
431
|
|
|
@@ -443,14 +440,6 @@ export function ConversationList({
|
|
|
443
440
|
|
|
444
441
|
const selectedKeyList = useMemo(() => Array.from(selectedKeys), [selectedKeys]);
|
|
445
442
|
|
|
446
|
-
const isSelectable = useCallback(
|
|
447
|
-
(item: ConversationItem) => {
|
|
448
|
-
if (view === "archived") return true;
|
|
449
|
-
return true;
|
|
450
|
-
},
|
|
451
|
-
[view]
|
|
452
|
-
);
|
|
453
|
-
|
|
454
443
|
const toggleSelected = useCallback(
|
|
455
444
|
(key: string) => {
|
|
456
445
|
setSelectedKeys((prev) => {
|
|
@@ -542,7 +531,7 @@ export function ConversationList({
|
|
|
542
531
|
confirmLabel: "Delete",
|
|
543
532
|
destructive: true,
|
|
544
533
|
});
|
|
545
|
-
}, [selectedKeyList
|
|
534
|
+
}, [selectedKeyList]);
|
|
546
535
|
|
|
547
536
|
const handleArchiveAll = useCallback(async () => {
|
|
548
537
|
if (view !== "active") return;
|
|
@@ -558,7 +547,7 @@ export function ConversationList({
|
|
|
558
547
|
description: `Archive ${keys.length} conversation(s) (current filter, only pending == 0). You can unarchive later.`,
|
|
559
548
|
confirmLabel: "Archive",
|
|
560
549
|
});
|
|
561
|
-
}, [view, filtered
|
|
550
|
+
}, [view, filtered]);
|
|
562
551
|
|
|
563
552
|
return (
|
|
564
553
|
<div
|
|
@@ -840,10 +829,9 @@ export function ConversationList({
|
|
|
840
829
|
bulkMode={bulkMode}
|
|
841
830
|
checked={selectedKeys.has(conversationKey(item))}
|
|
842
831
|
onToggleChecked={() => toggleSelected(conversationKey(item))}
|
|
843
|
-
view={view}
|
|
844
832
|
onClick={() => {
|
|
845
833
|
if (bulkMode) {
|
|
846
|
-
|
|
834
|
+
toggleSelected(conversationKey(item));
|
|
847
835
|
return;
|
|
848
836
|
}
|
|
849
837
|
onSelect(item.id, "group", item.name);
|
|
@@ -883,10 +871,9 @@ export function ConversationList({
|
|
|
883
871
|
bulkMode={bulkMode}
|
|
884
872
|
checked={selectedKeys.has(conversationKey(item))}
|
|
885
873
|
onToggleChecked={() => toggleSelected(conversationKey(item))}
|
|
886
|
-
view={view}
|
|
887
874
|
onClick={() => {
|
|
888
875
|
if (bulkMode) {
|
|
889
|
-
|
|
876
|
+
toggleSelected(conversationKey(item));
|
|
890
877
|
return;
|
|
891
878
|
}
|
|
892
879
|
onSelect(item.id, "agent", item.name);
|
|
@@ -1377,7 +1364,14 @@ function ConversationIconButton({
|
|
|
1377
1364
|
title={item.displayName}
|
|
1378
1365
|
>
|
|
1379
1366
|
{avatarUrl ? (
|
|
1380
|
-
<
|
|
1367
|
+
<Image
|
|
1368
|
+
src={avatarUrl}
|
|
1369
|
+
alt=""
|
|
1370
|
+
width={28}
|
|
1371
|
+
height={28}
|
|
1372
|
+
unoptimized
|
|
1373
|
+
className="h-7 w-7 rounded-full"
|
|
1374
|
+
/>
|
|
1381
1375
|
) : (
|
|
1382
1376
|
<span className="text-xl">{emoji}</span>
|
|
1383
1377
|
)}
|
|
@@ -1397,7 +1391,6 @@ function ConversationItemCard({
|
|
|
1397
1391
|
bulkMode,
|
|
1398
1392
|
checked,
|
|
1399
1393
|
onToggleChecked,
|
|
1400
|
-
view,
|
|
1401
1394
|
}: {
|
|
1402
1395
|
item: ConversationItem;
|
|
1403
1396
|
avatarUrl?: string;
|
|
@@ -1407,9 +1400,9 @@ function ConversationItemCard({
|
|
|
1407
1400
|
bulkMode?: boolean;
|
|
1408
1401
|
checked?: boolean;
|
|
1409
1402
|
onToggleChecked?: () => void;
|
|
1410
|
-
view?: "active" | "archived";
|
|
1411
1403
|
}) {
|
|
1412
1404
|
const emoji = item.type === "group" ? "👥" : getAgentEmoji(item.name);
|
|
1405
|
+
const showAgentTags = item.type === "agent" && (item.agentRuntime || item.projectName);
|
|
1413
1406
|
|
|
1414
1407
|
return (
|
|
1415
1408
|
<button
|
|
@@ -1437,7 +1430,14 @@ function ConversationItemCard({
|
|
|
1437
1430
|
<span className="relative h-9 w-9 shrink-0">
|
|
1438
1431
|
<span className="flex h-full w-full items-center justify-center rounded-full bg-white/55 ring-1 ring-white/40 text-[18px] overflow-hidden">
|
|
1439
1432
|
{avatarUrl ? (
|
|
1440
|
-
<
|
|
1433
|
+
<Image
|
|
1434
|
+
src={avatarUrl}
|
|
1435
|
+
alt=""
|
|
1436
|
+
width={36}
|
|
1437
|
+
height={36}
|
|
1438
|
+
unoptimized
|
|
1439
|
+
className="h-full w-full rounded-full"
|
|
1440
|
+
/>
|
|
1441
1441
|
) : (
|
|
1442
1442
|
emoji
|
|
1443
1443
|
)}
|
|
@@ -1458,6 +1458,20 @@ function ConversationItemCard({
|
|
|
1458
1458
|
</span>
|
|
1459
1459
|
)}
|
|
1460
1460
|
</div>
|
|
1461
|
+
{showAgentTags && (
|
|
1462
|
+
<div className="mt-0.5 flex flex-wrap gap-1">
|
|
1463
|
+
{item.agentRuntime && (
|
|
1464
|
+
<span className="inline-flex items-center rounded-full border bg-white/55 px-2 py-0.5 text-[10px] text-muted-foreground">
|
|
1465
|
+
{item.agentRuntime}
|
|
1466
|
+
</span>
|
|
1467
|
+
)}
|
|
1468
|
+
{item.projectName && (
|
|
1469
|
+
<span className="inline-flex items-center rounded-full border bg-white/55 px-2 py-0.5 text-[10px] text-muted-foreground">
|
|
1470
|
+
{item.projectName}
|
|
1471
|
+
</span>
|
|
1472
|
+
)}
|
|
1473
|
+
</div>
|
|
1474
|
+
)}
|
|
1461
1475
|
{item.lastMessage && (
|
|
1462
1476
|
<p className="text-[11px] text-muted-foreground whitespace-nowrap leading-4">
|
|
1463
1477
|
{truncateText(item.lastMessage.replace(/\n/g, ' '), 20)}
|
|
@@ -4,6 +4,7 @@ import { type ReactNode, useMemo } from "react";
|
|
|
4
4
|
import ReactMarkdown, { type Components } from "react-markdown";
|
|
5
5
|
import rehypeHighlight from "rehype-highlight";
|
|
6
6
|
import remarkGfm from "remark-gfm";
|
|
7
|
+
import Image from "next/image";
|
|
7
8
|
|
|
8
9
|
export function MarkdownRenderer({ children }: { children: string }) {
|
|
9
10
|
const normalized = useMemo(() => {
|
|
@@ -40,7 +41,17 @@ export function MarkdownRenderer({ children }: { children: string }) {
|
|
|
40
41
|
img: (props) => {
|
|
41
42
|
const safeSrc = typeof props.src === "string" ? props.src : undefined;
|
|
42
43
|
const safeAlt = typeof props.alt === "string" ? props.alt : "";
|
|
43
|
-
|
|
44
|
+
if (!safeSrc) return null;
|
|
45
|
+
return (
|
|
46
|
+
<Image
|
|
47
|
+
src={safeSrc}
|
|
48
|
+
alt={safeAlt}
|
|
49
|
+
width={1200}
|
|
50
|
+
height={800}
|
|
51
|
+
unoptimized
|
|
52
|
+
className="max-w-full h-auto"
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
44
55
|
},
|
|
45
56
|
table: ({ children }: { children?: ReactNode }) => (
|
|
46
57
|
<div className="mt-2 max-w-full overflow-auto">
|
package/src/hooks/use-avatar.ts
CHANGED
|
@@ -97,7 +97,6 @@ export function useConversationTimeline({
|
|
|
97
97
|
|
|
98
98
|
if (t0) {
|
|
99
99
|
const t1 = performance.now();
|
|
100
|
-
// eslint-disable-next-line no-console
|
|
101
100
|
console.log(
|
|
102
101
|
`[perf] bootstrapConversation type=${type} id=${id} items=${asc.length} queue=${res.queue.length} ${(t1 - t0).toFixed(1)}ms`
|
|
103
102
|
);
|
|
@@ -54,7 +54,6 @@ export function useMessageQueue({
|
|
|
54
54
|
setQueue(rows as QueuedMessage[]);
|
|
55
55
|
if (t0) {
|
|
56
56
|
const t1 = performance.now();
|
|
57
|
-
// eslint-disable-next-line no-console
|
|
58
57
|
console.log(
|
|
59
58
|
`[perf] fetchMessageQueue type=${type} id=${id} n=${rows.length} ${(t1 - t0).toFixed(1)}ms`
|
|
60
59
|
);
|
package/src/lib/actions.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
getAllAgents,
|
|
12
12
|
getConversationMetaMap,
|
|
13
13
|
getAgentDisplayNames,
|
|
14
|
+
getAgentEnv,
|
|
15
|
+
getAgentEnvMap,
|
|
14
16
|
upsertAgentDisplayName,
|
|
15
17
|
getAgentLastRequest,
|
|
16
18
|
getPendingCountByAgent,
|
|
@@ -47,6 +49,9 @@ import {
|
|
|
47
49
|
pinConversation,
|
|
48
50
|
unpinConversation,
|
|
49
51
|
listPinnedConversations,
|
|
52
|
+
getBotEnabledConversation,
|
|
53
|
+
setBotEnabledConversation,
|
|
54
|
+
listBotEnabledConversations,
|
|
50
55
|
type ConversationType,
|
|
51
56
|
type CueResponse,
|
|
52
57
|
} from "./db";
|
|
@@ -409,6 +414,19 @@ export async function claimWorkerLease(args: {
|
|
|
409
414
|
return acquireWorkerLease(args);
|
|
410
415
|
}
|
|
411
416
|
|
|
417
|
+
export async function fetchBotEnabled(type: ConversationType, id: string) {
|
|
418
|
+
return { enabled: getBotEnabledConversation(type, id) } as const;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function updateBotEnabled(type: ConversationType, id: string, enabled: boolean) {
|
|
422
|
+
setBotEnabledConversation(type, id, enabled);
|
|
423
|
+
return { success: true, enabled } as const;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function fetchBotEnabledConversations(limit?: number) {
|
|
427
|
+
return listBotEnabledConversations(limit);
|
|
428
|
+
}
|
|
429
|
+
|
|
412
430
|
// Responses
|
|
413
431
|
export async function submitResponse(
|
|
414
432
|
requestId: string,
|
|
@@ -561,6 +579,7 @@ export async function fetchConversationList(options?: {
|
|
|
561
579
|
: groupsAll;
|
|
562
580
|
|
|
563
581
|
const agentNameMap = getAgentDisplayNames(agents);
|
|
582
|
+
const agentEnvMap = getAgentEnvMap(agents);
|
|
564
583
|
|
|
565
584
|
const groupIds = groups.map((g) => g.id);
|
|
566
585
|
const groupMemberCounts = getGroupMemberCounts(groupIds);
|
|
@@ -647,6 +666,13 @@ export async function fetchConversationList(options?: {
|
|
|
647
666
|
id: agent,
|
|
648
667
|
name: agent,
|
|
649
668
|
displayName: agentNameMap[agent] || agent,
|
|
669
|
+
agentRuntime: agentEnvMap[agent]?.agent_runtime || undefined,
|
|
670
|
+
projectName: (() => {
|
|
671
|
+
const p = agentEnvMap[agent]?.project_dir;
|
|
672
|
+
if (!p) return undefined;
|
|
673
|
+
const base = path.basename(String(p));
|
|
674
|
+
return base || undefined;
|
|
675
|
+
})(),
|
|
650
676
|
pendingCount,
|
|
651
677
|
lastMessage: (lastIsResp ? respMsg : reqMsg)?.slice(0, 50),
|
|
652
678
|
lastTime: (lastIsResp ? lastRespTime : lastReqTime),
|
|
@@ -664,3 +690,20 @@ export async function fetchConversationList(options?: {
|
|
|
664
690
|
|
|
665
691
|
return items;
|
|
666
692
|
}
|
|
693
|
+
|
|
694
|
+
export async function fetchAgentEnv(agentId: string): Promise<{
|
|
695
|
+
agentRuntime?: string;
|
|
696
|
+
projectName?: string;
|
|
697
|
+
}> {
|
|
698
|
+
const id = String(agentId || "").trim();
|
|
699
|
+
if (!id) return {};
|
|
700
|
+
const env = getAgentEnv(id);
|
|
701
|
+
const agentRuntime = env?.agent_runtime || undefined;
|
|
702
|
+
const projectName = (() => {
|
|
703
|
+
const p = env?.project_dir;
|
|
704
|
+
if (!p) return undefined;
|
|
705
|
+
const base = path.basename(String(p));
|
|
706
|
+
return base || undefined;
|
|
707
|
+
})();
|
|
708
|
+
return { agentRuntime, projectName };
|
|
709
|
+
}
|
package/src/lib/avatar.ts
CHANGED
|
@@ -126,7 +126,6 @@ export async function thumbsAvatarDataUrl(seed: string): Promise<string> {
|
|
|
126
126
|
|
|
127
127
|
if (t0) {
|
|
128
128
|
const t2 = performance.now();
|
|
129
|
-
// eslint-disable-next-line no-console
|
|
130
129
|
console.log(
|
|
131
130
|
`[perf] thumbsAvatarDataUrl seed=${String(seed).slice(0, 8)} import=${(t1 - t0).toFixed(1)}ms encode=${(
|
|
132
131
|
t2 - t1
|
package/src/lib/db.ts
CHANGED
|
@@ -212,6 +212,16 @@ function initTables() {
|
|
|
212
212
|
)
|
|
213
213
|
`);
|
|
214
214
|
|
|
215
|
+
database.exec(`
|
|
216
|
+
CREATE TABLE IF NOT EXISTS bot_enabled_conversations (
|
|
217
|
+
conv_type TEXT NOT NULL,
|
|
218
|
+
conv_id TEXT NOT NULL,
|
|
219
|
+
enabled INTEGER NOT NULL,
|
|
220
|
+
updated_at DATETIME NOT NULL,
|
|
221
|
+
PRIMARY KEY (conv_type, conv_id)
|
|
222
|
+
)
|
|
223
|
+
`);
|
|
224
|
+
|
|
215
225
|
// Mode B: guide migrate and exit if an old DB exists.
|
|
216
226
|
const versionRow = database
|
|
217
227
|
.prepare(`SELECT value FROM schema_meta WHERE key = ?`)
|
|
@@ -286,6 +296,16 @@ function initTables() {
|
|
|
286
296
|
)
|
|
287
297
|
`);
|
|
288
298
|
|
|
299
|
+
database.exec(`
|
|
300
|
+
CREATE TABLE IF NOT EXISTS agent_envs (
|
|
301
|
+
agent_id TEXT PRIMARY KEY,
|
|
302
|
+
agent_runtime TEXT,
|
|
303
|
+
project_dir TEXT,
|
|
304
|
+
agent_terminal TEXT,
|
|
305
|
+
updated_at DATETIME
|
|
306
|
+
)
|
|
307
|
+
`);
|
|
308
|
+
|
|
289
309
|
database.exec(`
|
|
290
310
|
CREATE TABLE IF NOT EXISTS cue_message_queue (
|
|
291
311
|
id TEXT PRIMARY KEY,
|
|
@@ -411,6 +431,46 @@ export function acquireWorkerLease(args: {
|
|
|
411
431
|
|
|
412
432
|
export type ConversationType = "agent" | "group";
|
|
413
433
|
|
|
434
|
+
export function setBotEnabledConversation(convType: ConversationType, convId: string, enabled: boolean): void {
|
|
435
|
+
const t = convType === "group" ? "group" : "agent";
|
|
436
|
+
const id = String(convId || "").trim();
|
|
437
|
+
if (!id) return;
|
|
438
|
+
const now = nowIso();
|
|
439
|
+
getDb()
|
|
440
|
+
.prepare(
|
|
441
|
+
`INSERT INTO bot_enabled_conversations (conv_type, conv_id, enabled, updated_at)
|
|
442
|
+
VALUES (?, ?, ?, ?)
|
|
443
|
+
ON CONFLICT(conv_type, conv_id) DO UPDATE SET
|
|
444
|
+
enabled = excluded.enabled,
|
|
445
|
+
updated_at = excluded.updated_at`
|
|
446
|
+
)
|
|
447
|
+
.run(t, id, enabled ? 1 : 0, now);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function getBotEnabledConversation(convType: ConversationType, convId: string): boolean {
|
|
451
|
+
const t = convType === "group" ? "group" : "agent";
|
|
452
|
+
const id = String(convId || "").trim();
|
|
453
|
+
if (!id) return false;
|
|
454
|
+
const row = getDb()
|
|
455
|
+
.prepare(`SELECT enabled FROM bot_enabled_conversations WHERE conv_type = ? AND conv_id = ?`)
|
|
456
|
+
.get(t, id) as { enabled?: number } | undefined;
|
|
457
|
+
return Number(row?.enabled ?? 0) === 1;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function listBotEnabledConversations(limit?: number): Array<{ conv_type: ConversationType; conv_id: string }> {
|
|
461
|
+
const lim = Math.max(1, Math.min(500, Math.floor(Number(limit ?? 200))));
|
|
462
|
+
const rows = getDb()
|
|
463
|
+
.prepare(
|
|
464
|
+
`SELECT conv_type, conv_id
|
|
465
|
+
FROM bot_enabled_conversations
|
|
466
|
+
WHERE enabled = 1
|
|
467
|
+
ORDER BY updated_at DESC
|
|
468
|
+
LIMIT ?`
|
|
469
|
+
)
|
|
470
|
+
.all(lim) as Array<{ conv_type: ConversationType; conv_id: string }>;
|
|
471
|
+
return rows || [];
|
|
472
|
+
}
|
|
473
|
+
|
|
414
474
|
export interface CueQueuedMessage {
|
|
415
475
|
id: string;
|
|
416
476
|
conv_type: ConversationType;
|
|
@@ -859,6 +919,41 @@ export function getAgentDisplayNames(agentIds: string[]): Record<string, string>
|
|
|
859
919
|
return map;
|
|
860
920
|
}
|
|
861
921
|
|
|
922
|
+
export interface AgentEnv {
|
|
923
|
+
agent_id: string;
|
|
924
|
+
agent_runtime: string | null;
|
|
925
|
+
project_dir: string | null;
|
|
926
|
+
agent_terminal: string | null;
|
|
927
|
+
updated_at: string | null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export function getAgentEnvMap(agentIds: string[]): Record<string, AgentEnv> {
|
|
931
|
+
const unique = Array.from(new Set(agentIds.filter(Boolean)));
|
|
932
|
+
if (unique.length === 0) return {};
|
|
933
|
+
const placeholders = unique.map(() => "?").join(",");
|
|
934
|
+
const rows = getDb()
|
|
935
|
+
.prepare(
|
|
936
|
+
`SELECT agent_id, agent_runtime, project_dir, agent_terminal, updated_at
|
|
937
|
+
FROM agent_envs
|
|
938
|
+
WHERE agent_id IN (${placeholders})`
|
|
939
|
+
)
|
|
940
|
+
.all(...unique) as AgentEnv[];
|
|
941
|
+
const map: Record<string, AgentEnv> = {};
|
|
942
|
+
for (const r of rows) map[r.agent_id] = r;
|
|
943
|
+
return map;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export function getAgentEnv(agentId: string): AgentEnv | undefined {
|
|
947
|
+
const row = getDb()
|
|
948
|
+
.prepare(
|
|
949
|
+
`SELECT agent_id, agent_runtime, project_dir, agent_terminal, updated_at
|
|
950
|
+
FROM agent_envs
|
|
951
|
+
WHERE agent_id = ?`
|
|
952
|
+
)
|
|
953
|
+
.get(agentId) as AgentEnv | undefined;
|
|
954
|
+
return row;
|
|
955
|
+
}
|
|
956
|
+
|
|
862
957
|
// Type definitions
|
|
863
958
|
export interface CueRequest {
|
|
864
959
|
id: number;
|
package/src/lib/types.ts
CHANGED
package/src/types/chat.ts
CHANGED