cue-console 0.1.12 → 0.1.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cue-console",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Cue Hub console launcher (Next.js UI)",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": ["mcp", "cue", "console", "nextjs"],
@@ -38,7 +38,7 @@
38
38
  "@dicebear/core": "^9.2.4",
39
39
  "@dicebear/thumbs": "^9.2.4",
40
40
  "@fontsource-variable/source-sans-3": "^5.2.8",
41
- "better-sqlite3": "^12.5.0",
41
+ "better-sqlite3": "^12.6.0",
42
42
  "class-variance-authority": "^0.7.1",
43
43
  "clsx": "^2.1.1",
44
44
  "lucide-react": "^0.562.0",
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from "@/components/ui/dialog";
10
+ import { getAgentEmoji } from "@/lib/utils";
11
+
12
+ interface AvatarPickerDialogProps {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ target: { kind: "agent" | "group"; id: string } | null;
16
+ titleDisplay: string;
17
+ currentAvatarUrl: string;
18
+ candidates: { seed: string; url: string }[];
19
+ onRandomize: () => void;
20
+ onSelect: (seed: string) => void;
21
+ }
22
+
23
+ export function AvatarPickerDialog({
24
+ open,
25
+ onOpenChange,
26
+ target,
27
+ titleDisplay,
28
+ currentAvatarUrl,
29
+ candidates,
30
+ onRandomize,
31
+ onSelect,
32
+ }: AvatarPickerDialogProps) {
33
+ return (
34
+ <Dialog open={open} onOpenChange={onOpenChange}>
35
+ <DialogContent className="max-w-lg glass-surface glass-noise">
36
+ <DialogHeader>
37
+ <DialogTitle>Avatar</DialogTitle>
38
+ </DialogHeader>
39
+ {target && (
40
+ <div className="flex flex-col gap-4">
41
+ <div className="flex items-center gap-3">
42
+ <div className="h-14 w-14 rounded-full bg-muted overflow-hidden">
43
+ {currentAvatarUrl ? (
44
+ <img src={currentAvatarUrl} alt="" className="h-full w-full" />
45
+ ) : (
46
+ <div className="flex h-full w-full items-center justify-center text-xl">
47
+ {target.kind === "group" ? "👥" : getAgentEmoji(target.id)}
48
+ </div>
49
+ )}
50
+ </div>
51
+ <div className="flex-1 min-w-0">
52
+ <p className="text-sm font-semibold truncate">{titleDisplay}</p>
53
+ <p className="text-xs text-muted-foreground truncate">
54
+ Click a thumb to apply
55
+ </p>
56
+ </div>
57
+ <Button variant="outline" size="sm" onClick={onRandomize}>
58
+ Random
59
+ </Button>
60
+ </div>
61
+
62
+ <div className="max-h-52 overflow-y-auto pr-1">
63
+ <div className="grid grid-cols-5 gap-2">
64
+ {candidates.map((c) => (
65
+ <button
66
+ key={c.seed}
67
+ type="button"
68
+ className="h-12 w-12 rounded-full bg-muted overflow-hidden hover:ring-2 hover:ring-ring/40"
69
+ onClick={() => onSelect(c.seed)}
70
+ title="Apply"
71
+ >
72
+ {c.url ? (
73
+ <img src={c.url} alt="" className="h-full w-full" />
74
+ ) : null}
75
+ </button>
76
+ ))}
77
+ </div>
78
+ </div>
79
+ </div>
80
+ )}
81
+ </DialogContent>
82
+ </Dialog>
83
+ );
84
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { cn, getAgentEmoji } from "@/lib/utils";
6
+ import { ChevronLeft, Github } from "lucide-react";
7
+
8
+ interface ChatHeaderProps {
9
+ type: "agent" | "group";
10
+ id: string;
11
+ titleDisplay: string;
12
+ avatarUrl: string;
13
+ members: string[];
14
+ onBack?: () => void;
15
+ onAvatarClick: () => void;
16
+ onTitleChange: (newTitle: string) => Promise<void>;
17
+ }
18
+
19
+ export function ChatHeader({
20
+ type,
21
+ id,
22
+ titleDisplay,
23
+ avatarUrl,
24
+ members,
25
+ onBack,
26
+ onAvatarClick,
27
+ onTitleChange,
28
+ }: ChatHeaderProps) {
29
+ const [editingTitle, setEditingTitle] = useState(false);
30
+ const [titleDraft, setTitleDraft] = useState("");
31
+
32
+ const beginEditTitle = () => {
33
+ setEditingTitle(true);
34
+ setTitleDraft(titleDisplay);
35
+ setTimeout(() => {
36
+ const el = document.getElementById("chat-title-input");
37
+ if (el instanceof HTMLInputElement) el.focus();
38
+ }, 0);
39
+ };
40
+
41
+ const commitEditTitle = async () => {
42
+ const next = titleDraft.trim();
43
+ setEditingTitle(false);
44
+ if (!next || next === titleDisplay) return;
45
+ await onTitleChange(next);
46
+ };
47
+
48
+ return (
49
+ <div className={cn("px-4 pt-4")}>
50
+ <div
51
+ className={cn(
52
+ "mx-auto flex w-full max-w-230 items-center gap-2 rounded-3xl p-3",
53
+ "glass-surface glass-noise"
54
+ )}
55
+ >
56
+ {onBack && (
57
+ <Button variant="ghost" size="icon" onClick={onBack}>
58
+ <ChevronLeft className="h-5 w-5" />
59
+ </Button>
60
+ )}
61
+ <button
62
+ type="button"
63
+ className="h-9 w-9 shrink-0 rounded-full bg-muted overflow-hidden"
64
+ onClick={onAvatarClick}
65
+ title="Change avatar"
66
+ >
67
+ {avatarUrl ? (
68
+ <img src={avatarUrl} alt="" className="h-full w-full" />
69
+ ) : (
70
+ <span className="flex h-full w-full items-center justify-center text-lg">
71
+ {type === "group" ? "👥" : getAgentEmoji(id)}
72
+ </span>
73
+ )}
74
+ </button>
75
+ <div className="flex-1 min-w-0">
76
+ {editingTitle ? (
77
+ <input
78
+ id="chat-title-input"
79
+ value={titleDraft}
80
+ onChange={(e) => setTitleDraft(e.target.value)}
81
+ onKeyDown={(e) => {
82
+ if (e.key === "Enter") {
83
+ e.preventDefault();
84
+ void commitEditTitle();
85
+ }
86
+ if (e.key === "Escape") {
87
+ e.preventDefault();
88
+ setEditingTitle(false);
89
+ }
90
+ }}
91
+ onBlur={() => {
92
+ void commitEditTitle();
93
+ }}
94
+ className="w-60 max-w-full rounded-xl border border-white/45 bg-white/55 px-2.5 py-1.5 text-sm font-semibold outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]"
95
+ />
96
+ ) : (
97
+ <h2
98
+ className={cn("font-semibold", "cursor-text", "truncate")}
99
+ onDoubleClick={beginEditTitle}
100
+ title="Double-click to rename"
101
+ >
102
+ {titleDisplay}
103
+ </h2>
104
+ )}
105
+ {type === "group" && members.length > 0 && (
106
+ <p className="text-xs text-muted-foreground truncate">
107
+ {members.length} member{members.length === 1 ? "" : "s"}
108
+ </p>
109
+ )}
110
+ </div>
111
+ <Button variant="ghost" size="icon" asChild>
112
+ <a
113
+ href="https://github.com/nmhjklnm/cue-console"
114
+ target="_blank"
115
+ rel="noreferrer"
116
+ title="https://github.com/nmhjklnm/cue-console"
117
+ >
118
+ <Github className="h-5 w-5" />
119
+ </a>
120
+ </Button>
121
+ {type === "group" && (
122
+ <span
123
+ className="hidden sm:inline text-[11px] text-muted-foreground select-none mr-1"
124
+ title="Type @ to mention members"
125
+ >
126
+ @ mention
127
+ </span>
128
+ )}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ } from "@/components/ui/dialog";
9
+
10
+ interface ImagePreviewDialogProps {
11
+ image: { mime_type: string; base64_data: string } | null;
12
+ onClose: () => void;
13
+ }
14
+
15
+ export function ImagePreviewDialog({ image, onClose }: ImagePreviewDialogProps) {
16
+ return (
17
+ <Dialog open={!!image} onOpenChange={(open) => !open && onClose()}>
18
+ <DialogContent className="max-w-3xl glass-surface glass-noise">
19
+ <DialogHeader>
20
+ <DialogTitle>Preview</DialogTitle>
21
+ </DialogHeader>
22
+ {image && (
23
+ <div className="flex items-center justify-center">
24
+ <img
25
+ src={`data:${image.mime_type};base64,${image.base64_data}`}
26
+ alt=""
27
+ className="max-h-[70vh] rounded-lg"
28
+ />
29
+ </div>
30
+ )}
31
+ </DialogContent>
32
+ </Dialog>
33
+ );
34
+ }
@@ -0,0 +1,186 @@
1
+ import { useMemo } from "react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { Button } from "@/components/ui/button";
4
+ import { cn, getAgentEmoji, formatFullTime, getWaitingDuration } from "@/lib/utils";
5
+ import { MarkdownRenderer } from "@/components/markdown-renderer";
6
+ import { PayloadCard } from "@/components/payload-card";
7
+ import type { CueRequest } from "@/lib/actions";
8
+
9
+ interface MessageBubbleProps {
10
+ request: CueRequest;
11
+ showAgent?: boolean;
12
+ agentNameMap?: Record<string, string>;
13
+ avatarUrlMap?: Record<string, string>;
14
+ isHistory?: boolean;
15
+ showName?: boolean;
16
+ showAvatar?: boolean;
17
+ compact?: boolean;
18
+ disabled?: boolean;
19
+ currentInput?: string;
20
+ isGroup?: boolean;
21
+ onPasteChoice?: (text: string, mode?: "replace" | "append" | "upsert") => void;
22
+ onSubmitConfirm?: (requestId: string, text: string, cancelled: boolean) => void | Promise<void>;
23
+ onMentionAgent?: (agentId: string) => void;
24
+ onReply?: () => void;
25
+ onCancel?: () => void;
26
+ }
27
+
28
+ export function MessageBubble({
29
+ request,
30
+ showAgent,
31
+ agentNameMap,
32
+ avatarUrlMap,
33
+ isHistory,
34
+ showName,
35
+ showAvatar,
36
+ compact,
37
+ disabled,
38
+ currentInput,
39
+ isGroup,
40
+ onPasteChoice,
41
+ onSubmitConfirm,
42
+ onMentionAgent,
43
+ onReply,
44
+ onCancel,
45
+ }: MessageBubbleProps) {
46
+ const isPending = request.status === "PENDING";
47
+
48
+ const isPause = useMemo(() => {
49
+ if (!request.payload) return false;
50
+ try {
51
+ const obj = JSON.parse(request.payload) as Record<string, unknown>;
52
+ return obj?.type === "confirm" && obj?.variant === "pause";
53
+ } catch {
54
+ return false;
55
+ }
56
+ }, [request.payload]);
57
+
58
+ const selectedLines = useMemo(() => {
59
+ const text = (currentInput || "").trim();
60
+ if (!text) return new Set<string>();
61
+ return new Set(
62
+ text
63
+ .split(/\r?\n/)
64
+ .map((s) => s.trim())
65
+ .filter(Boolean)
66
+ );
67
+ }, [currentInput]);
68
+
69
+ const rawId = request.agent_id || "";
70
+ const displayName = (agentNameMap && rawId ? agentNameMap[rawId] || rawId : rawId) || "";
71
+ const cardMaxWidth = (showAvatar ?? true) ? "calc(100% - 3rem)" : "100%";
72
+ const avatarUrl = rawId && avatarUrlMap ? avatarUrlMap[`agent:${rawId}`] : "";
73
+
74
+ return (
75
+ <div
76
+ className={cn(
77
+ "flex max-w-full min-w-0 items-start gap-3",
78
+ compact && "gap-2",
79
+ isHistory && "opacity-60"
80
+ )}
81
+ >
82
+ {(showAvatar ?? true) ? (
83
+ <span
84
+ className={cn(
85
+ "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-lg",
86
+ isGroup && request.agent_id && onMentionAgent && "cursor-pointer"
87
+ )}
88
+ title={
89
+ isGroup && request.agent_id && onMentionAgent
90
+ ? "Double-click avatar to @mention"
91
+ : undefined
92
+ }
93
+ onDoubleClick={() => {
94
+ if (!isGroup) return;
95
+ const agentId = request.agent_id;
96
+ if (!agentId) return;
97
+ onMentionAgent?.(agentId);
98
+ }}
99
+ >
100
+ {avatarUrl ? (
101
+ <img src={avatarUrl} alt="" className="h-full w-full rounded-full" />
102
+ ) : (
103
+ getAgentEmoji(request.agent_id || "")
104
+ )}
105
+ </span>
106
+ ) : (
107
+ <span className="h-9 w-9 shrink-0" />
108
+ )}
109
+ <div className="flex-1 min-w-0 overflow-hidden">
110
+ {(showName ?? true) && (showAgent || displayName) && (
111
+ <p className="mb-1 text-xs text-muted-foreground truncate">{displayName}</p>
112
+ )}
113
+ <div
114
+ className={cn(
115
+ "rounded-3xl p-3 sm:p-4 max-w-full flex-1 basis-0 min-w-0 overflow-hidden",
116
+ "glass-surface-soft glass-noise",
117
+ isPending ? "ring-1 ring-ring/25" : "ring-1 ring-white/25"
118
+ )}
119
+ style={{ clipPath: "inset(0 round 1rem)", maxWidth: cardMaxWidth }}
120
+ >
121
+ <div className="text-sm wrap-anywhere overflow-hidden min-w-0">
122
+ <MarkdownRenderer>{request.prompt || ""}</MarkdownRenderer>
123
+ </div>
124
+ <PayloadCard
125
+ raw={request.payload}
126
+ disabled={disabled}
127
+ onPasteChoice={onPasteChoice}
128
+ onSubmitConfirm={(text, cancelled) =>
129
+ isPending ? onSubmitConfirm?.(request.request_id, text, cancelled) : undefined
130
+ }
131
+ selectedLines={selectedLines}
132
+ />
133
+ </div>
134
+ <div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
135
+ <span className="shrink-0">{formatFullTime(request.created_at || "")}</span>
136
+ {isPending && (
137
+ <>
138
+ <Badge variant="outline" className="text-xs shrink-0">
139
+ Waiting {getWaitingDuration(request.created_at || "")}
140
+ </Badge>
141
+ {!isPause && (
142
+ <>
143
+ <Badge variant="default" className="text-xs shrink-0">
144
+ Pending
145
+ </Badge>
146
+ {onReply && (
147
+ <Button
148
+ variant="link"
149
+ size="sm"
150
+ className="h-auto p-0 text-xs"
151
+ onClick={onReply}
152
+ disabled={disabled}
153
+ >
154
+ Reply
155
+ </Button>
156
+ )}
157
+ {onCancel && (
158
+ <Button
159
+ variant="link"
160
+ size="sm"
161
+ className="h-auto p-0 text-xs text-destructive"
162
+ onClick={onCancel}
163
+ disabled={disabled}
164
+ >
165
+ End
166
+ </Button>
167
+ )}
168
+ </>
169
+ )}
170
+ </>
171
+ )}
172
+ {request.status === "COMPLETED" && (
173
+ <Badge variant="secondary" className="text-xs shrink-0">
174
+ Replied
175
+ </Badge>
176
+ )}
177
+ {request.status === "CANCELLED" && (
178
+ <Badge variant="destructive" className="text-xs shrink-0">
179
+ Ended
180
+ </Badge>
181
+ )}
182
+ </div>
183
+ </div>
184
+ </div>
185
+ );
186
+ }