thepopebot 1.2.26 → 1.2.27

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.
@@ -7,6 +7,8 @@ import {
7
7
  getMessagesByChatId,
8
8
  deleteChat as dbDeleteChat,
9
9
  deleteAllChatsByUser,
10
+ updateChatTitle,
11
+ toggleChatStarred,
10
12
  } from '../db/chats.js';
11
13
  import {
12
14
  getNotifications as dbGetNotifications,
@@ -83,6 +85,37 @@ export async function deleteChat(chatId) {
83
85
  return { success: true };
84
86
  }
85
87
 
88
+ /**
89
+ * Rename a chat (with ownership check).
90
+ * @param {string} chatId
91
+ * @param {string} title
92
+ * @returns {Promise<{success: boolean}>}
93
+ */
94
+ export async function renameChat(chatId, title) {
95
+ const user = await requireAuth();
96
+ const chat = getChatById(chatId);
97
+ if (!chat || chat.userId !== user.id) {
98
+ return { success: false };
99
+ }
100
+ updateChatTitle(chatId, title);
101
+ return { success: true };
102
+ }
103
+
104
+ /**
105
+ * Toggle a chat's starred status (with ownership check).
106
+ * @param {string} chatId
107
+ * @returns {Promise<{success: boolean, starred?: number}>}
108
+ */
109
+ export async function starChat(chatId) {
110
+ const user = await requireAuth();
111
+ const chat = getChatById(chatId);
112
+ if (!chat || chat.userId !== user.id) {
113
+ return { success: false };
114
+ }
115
+ const starred = toggleChatStarred(chatId);
116
+ return { success: true, starred };
117
+ }
118
+
86
119
  /**
87
120
  * Delete all chats for the authenticated user.
88
121
  * @returns {Promise<{success: boolean}>}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
- import { SquarePenIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ClockIcon, ZapIcon } from "./icons.js";
4
+ import { SquarePenIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon } from "./icons.js";
5
5
  import { getUnreadNotificationCount } from "../actions.js";
6
6
  import { SidebarHistory } from "./sidebar-history.js";
7
7
  import { SidebarUserNav } from "./sidebar-user-nav.js";
@@ -113,38 +113,6 @@ function AppSidebar({ user }) {
113
113
  }
114
114
  ) }),
115
115
  collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Swarm" })
116
- ] }) }),
117
- /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
118
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
119
- SidebarMenuButton,
120
- {
121
- className: collapsed ? "justify-center" : "",
122
- onClick: () => {
123
- window.location.href = "/crons";
124
- },
125
- children: [
126
- /* @__PURE__ */ jsx(ClockIcon, { size: 16 }),
127
- !collapsed && /* @__PURE__ */ jsx("span", { children: "Cron Jobs" })
128
- ]
129
- }
130
- ) }),
131
- collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Cron Jobs" })
132
- ] }) }),
133
- /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
134
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
135
- SidebarMenuButton,
136
- {
137
- className: collapsed ? "justify-center" : "",
138
- onClick: () => {
139
- window.location.href = "/triggers";
140
- },
141
- children: [
142
- /* @__PURE__ */ jsx(ZapIcon, { size: 16 }),
143
- !collapsed && /* @__PURE__ */ jsx("span", { children: "Triggers" })
144
- ]
145
- }
146
- ) }),
147
- collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Triggers" })
148
116
  ] }) })
149
117
  ] })
150
118
  ] }),
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
- import { SquarePenIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ClockIcon, ZapIcon } from './icons.js';
4
+ import { SquarePenIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon } from './icons.js';
5
5
  import { getUnreadNotificationCount } from '../actions.js';
6
6
  import { SidebarHistory } from './sidebar-history.js';
7
7
  import { SidebarUserNav } from './sidebar-user-nav.js';
@@ -151,46 +151,6 @@ export function AppSidebar({ user }) {
151
151
  </Tooltip>
152
152
  </SidebarMenuItem>
153
153
 
154
- {/* Cron Jobs */}
155
- <SidebarMenuItem>
156
- <Tooltip>
157
- <TooltipTrigger asChild>
158
- <SidebarMenuButton
159
- className={collapsed ? 'justify-center' : ''}
160
- onClick={() => {
161
- window.location.href = '/crons';
162
- }}
163
- >
164
- <ClockIcon size={16} />
165
- {!collapsed && <span>Cron Jobs</span>}
166
- </SidebarMenuButton>
167
- </TooltipTrigger>
168
- {collapsed && (
169
- <TooltipContent side="right">Cron Jobs</TooltipContent>
170
- )}
171
- </Tooltip>
172
- </SidebarMenuItem>
173
-
174
- {/* Triggers */}
175
- <SidebarMenuItem>
176
- <Tooltip>
177
- <TooltipTrigger asChild>
178
- <SidebarMenuButton
179
- className={collapsed ? 'justify-center' : ''}
180
- onClick={() => {
181
- window.location.href = '/triggers';
182
- }}
183
- >
184
- <ZapIcon size={16} />
185
- {!collapsed && <span>Triggers</span>}
186
- </SidebarMenuButton>
187
- </TooltipTrigger>
188
- {collapsed && (
189
- <TooltipContent side="right">Triggers</TooltipContent>
190
- )}
191
- </Tooltip>
192
- </SidebarMenuItem>
193
-
194
154
  </SidebarMenu>
195
155
  </SidebarHeader>
196
156
 
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useRef } from "react";
4
4
  import { PageLayout } from "./page-layout.js";
5
- import { MessageIcon, TrashIcon, SearchIcon, PlusIcon } from "./icons.js";
6
- import { getChats, deleteChat } from "../actions.js";
5
+ import { MessageIcon, TrashIcon, SearchIcon, PlusIcon, MoreHorizontalIcon, StarIcon, StarFilledIcon, PencilIcon } from "./icons.js";
6
+ import { getChats, deleteChat, renameChat, starChat } from "../actions.js";
7
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "./ui/dropdown-menu.js";
7
8
  import { cn } from "../utils.js";
8
9
  function groupChatsByDate(chats) {
9
10
  const now = /* @__PURE__ */ new Date();
@@ -12,6 +13,7 @@ function groupChatsByDate(chats) {
12
13
  const last7Days = new Date(today.getTime() - 7 * 864e5);
13
14
  const last30Days = new Date(today.getTime() - 30 * 864e5);
14
15
  const groups = {
16
+ Starred: [],
15
17
  Today: [],
16
18
  Yesterday: [],
17
19
  "Last 7 Days": [],
@@ -19,6 +21,10 @@ function groupChatsByDate(chats) {
19
21
  Older: []
20
22
  };
21
23
  for (const chat of chats) {
24
+ if (chat.starred) {
25
+ groups.Starred.push(chat);
26
+ continue;
27
+ }
22
28
  const date = new Date(chat.updatedAt);
23
29
  if (date >= today) {
24
30
  groups.Today.push(chat);
@@ -72,10 +78,23 @@ function ChatsPage({ session }) {
72
78
  return () => window.removeEventListener("chatsupdated", handler);
73
79
  }, []);
74
80
  const handleDelete = async (chatId) => {
81
+ setChats((prev) => prev.filter((c) => c.id !== chatId));
75
82
  const { success } = await deleteChat(chatId);
76
- if (success) {
77
- setChats((prev) => prev.filter((c) => c.id !== chatId));
78
- }
83
+ if (!success) loadChats();
84
+ };
85
+ const handleStar = async (chatId) => {
86
+ setChats(
87
+ (prev) => prev.map((c) => c.id === chatId ? { ...c, starred: c.starred ? 0 : 1 } : c)
88
+ );
89
+ const { success } = await starChat(chatId);
90
+ if (!success) loadChats();
91
+ };
92
+ const handleRename = async (chatId, title) => {
93
+ setChats(
94
+ (prev) => prev.map((c) => c.id === chatId ? { ...c, title } : c)
95
+ );
96
+ const { success } = await renameChat(chatId, title);
97
+ if (!success) loadChats();
79
98
  };
80
99
  const filtered = query ? chats.filter((c) => c.title?.toLowerCase().includes(query.toLowerCase())) : chats;
81
100
  const grouped = groupChatsByDate(filtered);
@@ -120,7 +139,9 @@ function ChatsPage({ session }) {
120
139
  {
121
140
  chat,
122
141
  onNavigate: navigateToChat,
123
- onDelete: handleDelete
142
+ onDelete: handleDelete,
143
+ onStar: handleStar,
144
+ onRename: handleRename
124
145
  },
125
146
  chat.id
126
147
  )) })
@@ -128,39 +149,130 @@ function ChatsPage({ session }) {
128
149
  ) })
129
150
  ] });
130
151
  }
131
- function ChatRow({ chat, onNavigate, onDelete }) {
132
- const [showDelete, setShowDelete] = useState(false);
152
+ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
153
+ const [showMenu, setShowMenu] = useState(false);
154
+ const [editing, setEditing] = useState(false);
155
+ const [editTitle, setEditTitle] = useState(chat.title || "");
156
+ const inputRef = useRef(null);
157
+ useEffect(() => {
158
+ if (editing && inputRef.current) {
159
+ inputRef.current.focus();
160
+ inputRef.current.select();
161
+ }
162
+ }, [editing]);
163
+ const startRename = () => {
164
+ setEditTitle(chat.title || "");
165
+ setEditing(true);
166
+ };
167
+ const saveRename = () => {
168
+ const trimmed = editTitle.trim();
169
+ if (trimmed && trimmed !== chat.title) {
170
+ onRename(chat.id, trimmed);
171
+ }
172
+ setEditing(false);
173
+ };
174
+ const cancelRename = () => {
175
+ setEditing(false);
176
+ setEditTitle(chat.title || "");
177
+ };
133
178
  return /* @__PURE__ */ jsxs(
134
179
  "div",
135
180
  {
136
181
  className: "relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md",
137
- onMouseEnter: () => setShowDelete(true),
138
- onMouseLeave: () => setShowDelete(false),
139
- onClick: () => onNavigate(chat.id),
182
+ onMouseEnter: () => setShowMenu(true),
183
+ onMouseLeave: () => setShowMenu(false),
184
+ onClick: () => !editing && onNavigate(chat.id),
140
185
  children: [
141
186
  /* @__PURE__ */ jsx(MessageIcon, { size: 16 }),
142
187
  /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
143
- /* @__PURE__ */ jsx("span", { className: "text-sm truncate block", children: chat.title || "New Chat" }),
188
+ editing ? /* @__PURE__ */ jsx(
189
+ "input",
190
+ {
191
+ ref: inputRef,
192
+ type: "text",
193
+ value: editTitle,
194
+ onChange: (e) => setEditTitle(e.target.value),
195
+ onKeyDown: (e) => {
196
+ if (e.key === "Enter") saveRename();
197
+ if (e.key === "Escape") cancelRename();
198
+ },
199
+ onBlur: saveRename,
200
+ onClick: (e) => e.stopPropagation(),
201
+ className: "w-full text-sm bg-background border border-input rounded px-1.5 py-0.5 focus:outline-none focus:ring-2 focus:ring-ring"
202
+ }
203
+ ) : /* @__PURE__ */ jsx(
204
+ "span",
205
+ {
206
+ className: "text-sm truncate block",
207
+ onDoubleClick: (e) => {
208
+ e.stopPropagation();
209
+ startRename();
210
+ },
211
+ children: chat.title || "New Chat"
212
+ }
213
+ ),
144
214
  /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
145
215
  "Last message ",
146
216
  timeAgo(chat.updatedAt)
147
217
  ] })
148
218
  ] }),
149
- showDelete && /* @__PURE__ */ jsx(
150
- "button",
151
- {
152
- className: cn(
153
- "shrink-0 rounded-md p-1.5",
154
- "text-muted-foreground hover:text-destructive hover:bg-muted"
219
+ showMenu && !editing && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
220
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
221
+ "button",
222
+ {
223
+ className: cn(
224
+ "shrink-0 rounded-md p-1.5",
225
+ "text-muted-foreground hover:text-foreground hover:bg-muted"
226
+ ),
227
+ onClick: (e) => e.stopPropagation(),
228
+ "aria-label": "Chat options",
229
+ children: /* @__PURE__ */ jsx(MoreHorizontalIcon, { size: 14 })
230
+ }
231
+ ) }),
232
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", side: "bottom", children: [
233
+ /* @__PURE__ */ jsxs(
234
+ DropdownMenuItem,
235
+ {
236
+ onClick: (e) => {
237
+ e.stopPropagation();
238
+ onStar(chat.id);
239
+ },
240
+ children: [
241
+ chat.starred ? /* @__PURE__ */ jsx(StarFilledIcon, { size: 14 }) : /* @__PURE__ */ jsx(StarIcon, { size: 14 }),
242
+ chat.starred ? "Unstar" : "Star"
243
+ ]
244
+ }
245
+ ),
246
+ /* @__PURE__ */ jsxs(
247
+ DropdownMenuItem,
248
+ {
249
+ onClick: (e) => {
250
+ e.stopPropagation();
251
+ startRename();
252
+ },
253
+ children: [
254
+ /* @__PURE__ */ jsx(PencilIcon, { size: 14 }),
255
+ "Rename"
256
+ ]
257
+ }
155
258
  ),
156
- onClick: (e) => {
157
- e.stopPropagation();
158
- onDelete(chat.id);
159
- },
160
- "aria-label": "Delete chat",
161
- children: /* @__PURE__ */ jsx(TrashIcon, { size: 14 })
162
- }
163
- )
259
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
260
+ /* @__PURE__ */ jsxs(
261
+ DropdownMenuItem,
262
+ {
263
+ className: "text-destructive hover:text-destructive",
264
+ onClick: (e) => {
265
+ e.stopPropagation();
266
+ onDelete(chat.id);
267
+ },
268
+ children: [
269
+ /* @__PURE__ */ jsx(TrashIcon, { size: 14 }),
270
+ "Delete"
271
+ ]
272
+ }
273
+ )
274
+ ] })
275
+ ] })
164
276
  ]
165
277
  }
166
278
  );
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect } from 'react';
3
+ import { useState, useEffect, useRef } from 'react';
4
4
  import { PageLayout } from './page-layout.js';
5
- import { MessageIcon, TrashIcon, SearchIcon, PlusIcon } from './icons.js';
6
- import { getChats, deleteChat } from '../actions.js';
5
+ import { MessageIcon, TrashIcon, SearchIcon, PlusIcon, MoreHorizontalIcon, StarIcon, StarFilledIcon, PencilIcon } from './icons.js';
6
+ import { getChats, deleteChat, renameChat, starChat } from '../actions.js';
7
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from './ui/dropdown-menu.js';
7
8
  import { cn } from '../utils.js';
8
9
 
9
10
  function groupChatsByDate(chats) {
@@ -14,6 +15,7 @@ function groupChatsByDate(chats) {
14
15
  const last30Days = new Date(today.getTime() - 30 * 86400000);
15
16
 
16
17
  const groups = {
18
+ Starred: [],
17
19
  Today: [],
18
20
  Yesterday: [],
19
21
  'Last 7 Days': [],
@@ -22,6 +24,10 @@ function groupChatsByDate(chats) {
22
24
  };
23
25
 
24
26
  for (const chat of chats) {
27
+ if (chat.starred) {
28
+ groups.Starred.push(chat);
29
+ continue;
30
+ }
25
31
  const date = new Date(chat.updatedAt);
26
32
  if (date >= today) {
27
33
  groups.Today.push(chat);
@@ -83,10 +89,25 @@ export function ChatsPage({ session }) {
83
89
  }, []);
84
90
 
85
91
  const handleDelete = async (chatId) => {
92
+ setChats((prev) => prev.filter((c) => c.id !== chatId));
86
93
  const { success } = await deleteChat(chatId);
87
- if (success) {
88
- setChats((prev) => prev.filter((c) => c.id !== chatId));
89
- }
94
+ if (!success) loadChats();
95
+ };
96
+
97
+ const handleStar = async (chatId) => {
98
+ setChats((prev) =>
99
+ prev.map((c) => (c.id === chatId ? { ...c, starred: c.starred ? 0 : 1 } : c))
100
+ );
101
+ const { success } = await starChat(chatId);
102
+ if (!success) loadChats();
103
+ };
104
+
105
+ const handleRename = async (chatId, title) => {
106
+ setChats((prev) =>
107
+ prev.map((c) => (c.id === chatId ? { ...c, title } : c))
108
+ );
109
+ const { success } = await renameChat(chatId, title);
110
+ if (!success) loadChats();
90
111
  };
91
112
 
92
113
  const filtered = query
@@ -154,6 +175,8 @@ export function ChatsPage({ session }) {
154
175
  chat={chat}
155
176
  onNavigate={navigateToChat}
156
177
  onDelete={handleDelete}
178
+ onStar={handleStar}
179
+ onRename={handleRename}
157
180
  />
158
181
  ))}
159
182
  </div>
@@ -166,37 +189,121 @@ export function ChatsPage({ session }) {
166
189
  );
167
190
  }
168
191
 
169
- function ChatRow({ chat, onNavigate, onDelete }) {
170
- const [showDelete, setShowDelete] = useState(false);
192
+ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
193
+ const [showMenu, setShowMenu] = useState(false);
194
+ const [editing, setEditing] = useState(false);
195
+ const [editTitle, setEditTitle] = useState(chat.title || '');
196
+ const inputRef = useRef(null);
197
+
198
+ useEffect(() => {
199
+ if (editing && inputRef.current) {
200
+ inputRef.current.focus();
201
+ inputRef.current.select();
202
+ }
203
+ }, [editing]);
204
+
205
+ const startRename = () => {
206
+ setEditTitle(chat.title || '');
207
+ setEditing(true);
208
+ };
209
+
210
+ const saveRename = () => {
211
+ const trimmed = editTitle.trim();
212
+ if (trimmed && trimmed !== chat.title) {
213
+ onRename(chat.id, trimmed);
214
+ }
215
+ setEditing(false);
216
+ };
217
+
218
+ const cancelRename = () => {
219
+ setEditing(false);
220
+ setEditTitle(chat.title || '');
221
+ };
171
222
 
172
223
  return (
173
224
  <div
174
225
  className="relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md"
175
- onMouseEnter={() => setShowDelete(true)}
176
- onMouseLeave={() => setShowDelete(false)}
177
- onClick={() => onNavigate(chat.id)}
226
+ onMouseEnter={() => setShowMenu(true)}
227
+ onMouseLeave={() => setShowMenu(false)}
228
+ onClick={() => !editing && onNavigate(chat.id)}
178
229
  >
179
230
  <MessageIcon size={16} />
180
231
  <div className="flex-1 min-w-0">
181
- <span className="text-sm truncate block">{chat.title || 'New Chat'}</span>
232
+ {editing ? (
233
+ <input
234
+ ref={inputRef}
235
+ type="text"
236
+ value={editTitle}
237
+ onChange={(e) => setEditTitle(e.target.value)}
238
+ onKeyDown={(e) => {
239
+ if (e.key === 'Enter') saveRename();
240
+ if (e.key === 'Escape') cancelRename();
241
+ }}
242
+ onBlur={saveRename}
243
+ onClick={(e) => e.stopPropagation()}
244
+ className="w-full text-sm bg-background border border-input rounded px-1.5 py-0.5 focus:outline-none focus:ring-2 focus:ring-ring"
245
+ />
246
+ ) : (
247
+ <span
248
+ className="text-sm truncate block"
249
+ onDoubleClick={(e) => {
250
+ e.stopPropagation();
251
+ startRename();
252
+ }}
253
+ >
254
+ {chat.title || 'New Chat'}
255
+ </span>
256
+ )}
182
257
  <span className="text-xs text-muted-foreground">
183
258
  Last message {timeAgo(chat.updatedAt)}
184
259
  </span>
185
260
  </div>
186
- {showDelete && (
187
- <button
188
- className={cn(
189
- 'shrink-0 rounded-md p-1.5',
190
- 'text-muted-foreground hover:text-destructive hover:bg-muted'
191
- )}
192
- onClick={(e) => {
193
- e.stopPropagation();
194
- onDelete(chat.id);
195
- }}
196
- aria-label="Delete chat"
197
- >
198
- <TrashIcon size={14} />
199
- </button>
261
+ {showMenu && !editing && (
262
+ <DropdownMenu>
263
+ <DropdownMenuTrigger asChild>
264
+ <button
265
+ className={cn(
266
+ 'shrink-0 rounded-md p-1.5',
267
+ 'text-muted-foreground hover:text-foreground hover:bg-muted'
268
+ )}
269
+ onClick={(e) => e.stopPropagation()}
270
+ aria-label="Chat options"
271
+ >
272
+ <MoreHorizontalIcon size={14} />
273
+ </button>
274
+ </DropdownMenuTrigger>
275
+ <DropdownMenuContent align="end" side="bottom">
276
+ <DropdownMenuItem
277
+ onClick={(e) => {
278
+ e.stopPropagation();
279
+ onStar(chat.id);
280
+ }}
281
+ >
282
+ {chat.starred ? <StarFilledIcon size={14} /> : <StarIcon size={14} />}
283
+ {chat.starred ? 'Unstar' : 'Star'}
284
+ </DropdownMenuItem>
285
+ <DropdownMenuItem
286
+ onClick={(e) => {
287
+ e.stopPropagation();
288
+ startRename();
289
+ }}
290
+ >
291
+ <PencilIcon size={14} />
292
+ Rename
293
+ </DropdownMenuItem>
294
+ <DropdownMenuSeparator />
295
+ <DropdownMenuItem
296
+ className="text-destructive hover:text-destructive"
297
+ onClick={(e) => {
298
+ e.stopPropagation();
299
+ onDelete(chat.id);
300
+ }}
301
+ >
302
+ <TrashIcon size={14} />
303
+ Delete
304
+ </DropdownMenuItem>
305
+ </DropdownMenuContent>
306
+ </DropdownMenu>
200
307
  )}
201
308
  </div>
202
309
  );
@@ -1,7 +1,6 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
- import { PageLayout } from "./page-layout.js";
5
4
  import { ClockIcon, SpinnerIcon, ChevronDownIcon } from "./icons.js";
6
5
  import { getSwarmConfig } from "../actions.js";
7
6
  function describeCron(schedule) {
@@ -41,6 +40,24 @@ const typeBadgeStyles = {
41
40
  command: "bg-blue-500/10 text-blue-500",
42
41
  webhook: "bg-orange-500/10 text-orange-500"
43
42
  };
43
+ const typeOrder = { agent: 0, command: 1, webhook: 2 };
44
+ function sortByType(items) {
45
+ return [...items].sort((a, b) => {
46
+ const ta = typeOrder[a.type || "agent"] ?? 99;
47
+ const tb = typeOrder[b.type || "agent"] ?? 99;
48
+ return ta - tb;
49
+ });
50
+ }
51
+ function GroupHeader({ label, count }) {
52
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2 pb-1", children: [
53
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: label }),
54
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
55
+ "(",
56
+ count,
57
+ ")"
58
+ ] })
59
+ ] });
60
+ }
44
61
  function CronCard({ cron }) {
45
62
  const [expanded, setExpanded] = useState(false);
46
63
  const type = cron.type || "agent";
@@ -106,7 +123,7 @@ function CronCard({ cron }) {
106
123
  }
107
124
  );
108
125
  }
109
- function CronsPage({ session }) {
126
+ function CronsPage() {
110
127
  const [crons, setCrons] = useState([]);
111
128
  const [loading, setLoading] = useState(true);
112
129
  useEffect(() => {
@@ -115,18 +132,16 @@ function CronsPage({ session }) {
115
132
  }).catch(() => {
116
133
  }).finally(() => setLoading(false));
117
134
  }, []);
118
- const enabledCount = crons.filter((c) => c.enabled !== false).length;
119
- return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
120
- /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
121
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Cron Jobs" }),
122
- !loading && /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [
123
- crons.length,
124
- " job",
125
- crons.length !== 1 ? "s" : "",
126
- " configured, ",
127
- enabledCount,
128
- " enabled"
129
- ] })
135
+ const enabled = sortByType(crons.filter((c) => c.enabled !== false));
136
+ const disabled = sortByType(crons.filter((c) => c.enabled === false));
137
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
138
+ !loading && /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mb-4", children: [
139
+ crons.length,
140
+ " job",
141
+ crons.length !== 1 ? "s" : "",
142
+ " configured, ",
143
+ enabled.length,
144
+ " enabled"
130
145
  ] }),
131
146
  loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-lg bg-border/50" }, i)) }) : crons.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [
132
147
  /* @__PURE__ */ jsx("div", { className: "rounded-full bg-muted p-4 mb-4", children: /* @__PURE__ */ jsx(ClockIcon, { size: 24 }) }),
@@ -136,7 +151,16 @@ function CronsPage({ session }) {
136
151
  /* @__PURE__ */ jsx("span", { className: "font-mono", children: "config/CRONS.json" }),
137
152
  " in your project."
138
153
  ] })
139
- ] }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: crons.map((cron, i) => /* @__PURE__ */ jsx(CronCard, { cron }, i)) })
154
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
155
+ enabled.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
156
+ /* @__PURE__ */ jsx(GroupHeader, { label: "Enabled", count: enabled.length }),
157
+ enabled.map((cron, i) => /* @__PURE__ */ jsx(CronCard, { cron }, `enabled-${i}`))
158
+ ] }),
159
+ disabled.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
160
+ /* @__PURE__ */ jsx(GroupHeader, { label: "Disabled", count: disabled.length }),
161
+ disabled.map((cron, i) => /* @__PURE__ */ jsx(CronCard, { cron }, `disabled-${i}`))
162
+ ] })
163
+ ] })
140
164
  ] });
141
165
  }
142
166
  export {