gigaclaw 1.4.0

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.
Files changed (249) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +237 -0
  3. package/api/CLAUDE.md +19 -0
  4. package/api/index.js +265 -0
  5. package/bin/cli.js +823 -0
  6. package/bin/local.sh +85 -0
  7. package/bin/postinstall.js +63 -0
  8. package/config/index.js +26 -0
  9. package/config/instrumentation.js +62 -0
  10. package/drizzle/0000_initial.sql +52 -0
  11. package/drizzle/0001_nostalgic_sersi.sql +11 -0
  12. package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
  13. package/drizzle/0003_rename_code_workspaces.sql +5 -0
  14. package/drizzle/meta/0000_snapshot.json +321 -0
  15. package/drizzle/meta/0001_snapshot.json +390 -0
  16. package/drizzle/meta/0002_snapshot.json +411 -0
  17. package/drizzle/meta/0003_snapshot.json +419 -0
  18. package/drizzle/meta/_journal.json +34 -0
  19. package/lib/actions.js +44 -0
  20. package/lib/ai/agent.js +86 -0
  21. package/lib/ai/index.js +342 -0
  22. package/lib/ai/model.js +180 -0
  23. package/lib/ai/tools.js +269 -0
  24. package/lib/ai/web-search.js +42 -0
  25. package/lib/auth/actions.js +28 -0
  26. package/lib/auth/config.js +27 -0
  27. package/lib/auth/edge-config.js +27 -0
  28. package/lib/auth/index.js +27 -0
  29. package/lib/auth/middleware.js +62 -0
  30. package/lib/channels/base.js +56 -0
  31. package/lib/channels/index.js +15 -0
  32. package/lib/channels/telegram.js +148 -0
  33. package/lib/chat/actions.js +579 -0
  34. package/lib/chat/api.js +140 -0
  35. package/lib/chat/components/app-sidebar.js +213 -0
  36. package/lib/chat/components/app-sidebar.jsx +279 -0
  37. package/lib/chat/components/chat-header.js +192 -0
  38. package/lib/chat/components/chat-header.jsx +223 -0
  39. package/lib/chat/components/chat-input.js +236 -0
  40. package/lib/chat/components/chat-input.jsx +249 -0
  41. package/lib/chat/components/chat-nav-context.js +11 -0
  42. package/lib/chat/components/chat-nav-context.jsx +11 -0
  43. package/lib/chat/components/chat-page.js +99 -0
  44. package/lib/chat/components/chat-page.jsx +121 -0
  45. package/lib/chat/components/chat.js +153 -0
  46. package/lib/chat/components/chat.jsx +199 -0
  47. package/lib/chat/components/chats-page.js +367 -0
  48. package/lib/chat/components/chats-page.jsx +394 -0
  49. package/lib/chat/components/code-mode-toggle.js +132 -0
  50. package/lib/chat/components/code-mode-toggle.jsx +163 -0
  51. package/lib/chat/components/crons-page.js +172 -0
  52. package/lib/chat/components/crons-page.jsx +244 -0
  53. package/lib/chat/components/greeting.js +11 -0
  54. package/lib/chat/components/greeting.jsx +16 -0
  55. package/lib/chat/components/icons.js +805 -0
  56. package/lib/chat/components/icons.jsx +751 -0
  57. package/lib/chat/components/index.js +20 -0
  58. package/lib/chat/components/message.js +363 -0
  59. package/lib/chat/components/message.jsx +422 -0
  60. package/lib/chat/components/messages.js +65 -0
  61. package/lib/chat/components/messages.jsx +74 -0
  62. package/lib/chat/components/notifications-page.js +56 -0
  63. package/lib/chat/components/notifications-page.jsx +87 -0
  64. package/lib/chat/components/page-layout.js +21 -0
  65. package/lib/chat/components/page-layout.jsx +28 -0
  66. package/lib/chat/components/pull-requests-page.js +103 -0
  67. package/lib/chat/components/pull-requests-page.jsx +113 -0
  68. package/lib/chat/components/settings-layout.js +39 -0
  69. package/lib/chat/components/settings-layout.jsx +53 -0
  70. package/lib/chat/components/settings-secrets-page.js +216 -0
  71. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  72. package/lib/chat/components/sidebar-history-item.js +138 -0
  73. package/lib/chat/components/sidebar-history-item.jsx +119 -0
  74. package/lib/chat/components/sidebar-history.js +167 -0
  75. package/lib/chat/components/sidebar-history.jsx +220 -0
  76. package/lib/chat/components/sidebar-user-nav.js +61 -0
  77. package/lib/chat/components/sidebar-user-nav.jsx +77 -0
  78. package/lib/chat/components/swarm-page.js +157 -0
  79. package/lib/chat/components/swarm-page.jsx +210 -0
  80. package/lib/chat/components/tool-call.js +89 -0
  81. package/lib/chat/components/tool-call.jsx +107 -0
  82. package/lib/chat/components/triggers-page.js +153 -0
  83. package/lib/chat/components/triggers-page.jsx +221 -0
  84. package/lib/chat/components/ui/combobox.js +98 -0
  85. package/lib/chat/components/ui/combobox.jsx +114 -0
  86. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  87. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  88. package/lib/chat/components/ui/dropdown-menu.js +194 -0
  89. package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
  90. package/lib/chat/components/ui/rename-dialog.js +78 -0
  91. package/lib/chat/components/ui/rename-dialog.jsx +74 -0
  92. package/lib/chat/components/ui/scroll-area.js +13 -0
  93. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  94. package/lib/chat/components/ui/separator.js +21 -0
  95. package/lib/chat/components/ui/separator.jsx +18 -0
  96. package/lib/chat/components/ui/sheet.js +75 -0
  97. package/lib/chat/components/ui/sheet.jsx +95 -0
  98. package/lib/chat/components/ui/sidebar.js +228 -0
  99. package/lib/chat/components/ui/sidebar.jsx +246 -0
  100. package/lib/chat/components/ui/tooltip.js +56 -0
  101. package/lib/chat/components/ui/tooltip.jsx +66 -0
  102. package/lib/chat/components/upgrade-dialog.js +151 -0
  103. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  104. package/lib/chat/utils.js +11 -0
  105. package/lib/code/actions.js +153 -0
  106. package/lib/code/code-page.js +22 -0
  107. package/lib/code/code-page.jsx +25 -0
  108. package/lib/code/index.js +1 -0
  109. package/lib/code/terminal-view.js +201 -0
  110. package/lib/code/terminal-view.jsx +224 -0
  111. package/lib/code/ws-proxy.js +80 -0
  112. package/lib/cron.js +246 -0
  113. package/lib/db/api-keys.js +163 -0
  114. package/lib/db/chats.js +168 -0
  115. package/lib/db/code-workspaces.js +110 -0
  116. package/lib/db/index.js +52 -0
  117. package/lib/db/notifications.js +99 -0
  118. package/lib/db/schema.js +66 -0
  119. package/lib/db/update-check.js +96 -0
  120. package/lib/db/users.js +89 -0
  121. package/lib/paths.js +42 -0
  122. package/lib/tools/create-job.js +97 -0
  123. package/lib/tools/docker.js +146 -0
  124. package/lib/tools/github.js +271 -0
  125. package/lib/tools/openai.js +35 -0
  126. package/lib/tools/telegram.js +292 -0
  127. package/lib/triggers.js +104 -0
  128. package/lib/utils/render-md.js +111 -0
  129. package/package.json +118 -0
  130. package/setup/lib/auth.mjs +81 -0
  131. package/setup/lib/env.mjs +21 -0
  132. package/setup/lib/fs-utils.mjs +20 -0
  133. package/setup/lib/github.mjs +149 -0
  134. package/setup/lib/prerequisites.mjs +155 -0
  135. package/setup/lib/prompts.mjs +267 -0
  136. package/setup/lib/providers.mjs +105 -0
  137. package/setup/lib/sync.mjs +125 -0
  138. package/setup/lib/targets.mjs +45 -0
  139. package/setup/lib/telegram-verify.mjs +63 -0
  140. package/setup/lib/telegram.mjs +76 -0
  141. package/setup/setup-cloud.mjs +833 -0
  142. package/setup/setup-local.mjs +377 -0
  143. package/setup/setup-telegram.mjs +265 -0
  144. package/setup/setup.mjs +87 -0
  145. package/templates/.dockerignore +5 -0
  146. package/templates/.env.example +104 -0
  147. package/templates/.github/workflows/auto-merge.yml +117 -0
  148. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  149. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  150. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  151. package/templates/.github/workflows/run-job.yml +89 -0
  152. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  153. package/templates/.gitignore.template +45 -0
  154. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  155. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  156. package/templates/CLAUDE.md +29 -0
  157. package/templates/CLAUDE.md.template +308 -0
  158. package/templates/app/api/[...gigaclaw]/route.js +1 -0
  159. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  160. package/templates/app/chat/[chatId]/page.js +9 -0
  161. package/templates/app/chats/page.js +7 -0
  162. package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
  163. package/templates/app/components/ascii-logo.jsx +12 -0
  164. package/templates/app/components/login-form.jsx +92 -0
  165. package/templates/app/components/setup-form.jsx +82 -0
  166. package/templates/app/components/theme-provider.jsx +11 -0
  167. package/templates/app/components/theme-toggle.jsx +38 -0
  168. package/templates/app/components/ui/button.jsx +21 -0
  169. package/templates/app/components/ui/card.jsx +23 -0
  170. package/templates/app/components/ui/input.jsx +10 -0
  171. package/templates/app/components/ui/label.jsx +10 -0
  172. package/templates/app/crons/page.js +5 -0
  173. package/templates/app/globals.css +90 -0
  174. package/templates/app/layout.js +33 -0
  175. package/templates/app/login/page.js +15 -0
  176. package/templates/app/notifications/page.js +7 -0
  177. package/templates/app/page.js +7 -0
  178. package/templates/app/pull-requests/page.js +7 -0
  179. package/templates/app/settings/crons/page.js +5 -0
  180. package/templates/app/settings/layout.js +7 -0
  181. package/templates/app/settings/page.js +5 -0
  182. package/templates/app/settings/secrets/page.js +5 -0
  183. package/templates/app/settings/triggers/page.js +5 -0
  184. package/templates/app/stream/chat/route.js +1 -0
  185. package/templates/app/swarm/page.js +7 -0
  186. package/templates/app/triggers/page.js +5 -0
  187. package/templates/config/CODE_PLANNING.md +14 -0
  188. package/templates/config/CRONS.json +56 -0
  189. package/templates/config/HEARTBEAT.md +3 -0
  190. package/templates/config/JOB_AGENT.md +30 -0
  191. package/templates/config/JOB_PLANNING.md +240 -0
  192. package/templates/config/JOB_SUMMARY.md +130 -0
  193. package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
  194. package/templates/config/SOUL.md +48 -0
  195. package/templates/config/TRIGGERS.json +58 -0
  196. package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
  197. package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
  198. package/templates/docker/claude-code-job/Dockerfile +34 -0
  199. package/templates/docker/claude-code-job/entrypoint.sh +149 -0
  200. package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
  201. package/templates/docker/claude-code-workspace/Dockerfile +61 -0
  202. package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
  203. package/templates/docker/event-handler/Dockerfile +20 -0
  204. package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
  205. package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
  206. package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
  207. package/templates/docker-compose.local.yml +78 -0
  208. package/templates/docker-compose.yml +64 -0
  209. package/templates/instrumentation.js +6 -0
  210. package/templates/middleware.js +23 -0
  211. package/templates/next.config.mjs +3 -0
  212. package/templates/postcss.config.mjs +5 -0
  213. package/templates/public/favicon.ico +0 -0
  214. package/templates/server.js +25 -0
  215. package/templates/skills/LICENSE +21 -0
  216. package/templates/skills/README.md +119 -0
  217. package/templates/skills/brave-search/SKILL.md +79 -0
  218. package/templates/skills/brave-search/content.js +86 -0
  219. package/templates/skills/brave-search/package-lock.json +621 -0
  220. package/templates/skills/brave-search/package.json +14 -0
  221. package/templates/skills/brave-search/search.js +199 -0
  222. package/templates/skills/browser-tools/SKILL.md +196 -0
  223. package/templates/skills/browser-tools/browser-content.js +103 -0
  224. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  225. package/templates/skills/browser-tools/browser-eval.js +53 -0
  226. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  227. package/templates/skills/browser-tools/browser-nav.js +44 -0
  228. package/templates/skills/browser-tools/browser-pick.js +162 -0
  229. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  230. package/templates/skills/browser-tools/browser-start.js +87 -0
  231. package/templates/skills/browser-tools/package-lock.json +2556 -0
  232. package/templates/skills/browser-tools/package.json +19 -0
  233. package/templates/skills/google-docs/SKILL.md +23 -0
  234. package/templates/skills/google-docs/create.sh +69 -0
  235. package/templates/skills/google-drive/SKILL.md +47 -0
  236. package/templates/skills/google-drive/delete.sh +47 -0
  237. package/templates/skills/google-drive/download.sh +50 -0
  238. package/templates/skills/google-drive/list.sh +41 -0
  239. package/templates/skills/google-drive/upload.sh +76 -0
  240. package/templates/skills/kie-ai/SKILL.md +38 -0
  241. package/templates/skills/kie-ai/generate-image.sh +77 -0
  242. package/templates/skills/kie-ai/generate-video.sh +69 -0
  243. package/templates/skills/llm-secrets/SKILL.md +34 -0
  244. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  245. package/templates/skills/modify-self/SKILL.md +12 -0
  246. package/templates/skills/youtube-transcript/SKILL.md +41 -0
  247. package/templates/skills/youtube-transcript/package-lock.json +24 -0
  248. package/templates/skills/youtube-transcript/package.json +8 -0
  249. package/templates/skills/youtube-transcript/transcript.js +84 -0
@@ -0,0 +1,199 @@
1
+ 'use client';
2
+
3
+ import { useChat } from '@ai-sdk/react';
4
+ import { DefaultChatTransport } from 'ai';
5
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
6
+ import { Messages } from './messages.js';
7
+ import { ChatInput } from './chat-input.js';
8
+ import { ChatHeader } from './chat-header.js';
9
+ import { Greeting } from './greeting.js';
10
+ import { CodeModeToggle } from './code-mode-toggle.js';
11
+ import { getRepositories, getBranches } from '../actions.js';
12
+
13
+ export function Chat({ chatId, initialMessages = [], workspace = null }) {
14
+ const [input, setInput] = useState('');
15
+ const [files, setFiles] = useState([]);
16
+ const hasNavigated = useRef(false);
17
+ const [codeMode, setCodeMode] = useState(!!workspace);
18
+ const [repo, setRepo] = useState(workspace?.repo || '');
19
+ const [branch, setBranch] = useState(workspace?.branch || '');
20
+
21
+ const codeModeRef = useRef({ codeMode, repo, branch });
22
+ codeModeRef.current = { codeMode, repo, branch };
23
+
24
+ const transport = useMemo(
25
+ () =>
26
+ new DefaultChatTransport({
27
+ api: '/stream/chat',
28
+ body: () => ({
29
+ chatId,
30
+ ...(codeModeRef.current.codeMode && codeModeRef.current.repo && codeModeRef.current.branch
31
+ ? { codeMode: true, repo: codeModeRef.current.repo, branch: codeModeRef.current.branch }
32
+ : {}),
33
+ }),
34
+ }),
35
+ [chatId]
36
+ );
37
+
38
+ const {
39
+ messages,
40
+ status,
41
+ stop,
42
+ error,
43
+ sendMessage,
44
+ regenerate,
45
+ setMessages,
46
+ } = useChat({
47
+ id: chatId,
48
+ messages: initialMessages,
49
+ transport,
50
+ onError: (err) => console.error('Chat error:', err),
51
+ });
52
+
53
+ // After first message sent, update URL and notify sidebar
54
+ useEffect(() => {
55
+ if (!hasNavigated.current && messages.length >= 1 && status !== 'ready' && window.location.pathname !== `/chat/${chatId}`) {
56
+ hasNavigated.current = true;
57
+ window.history.replaceState({}, '', `/chat/${chatId}`);
58
+ window.dispatchEvent(new Event('chatsupdated'));
59
+ // Dispatch again after delay to pick up async title update
60
+ setTimeout(() => window.dispatchEvent(new Event('chatsupdated')), 5000);
61
+ }
62
+ }, [messages.length, status, chatId]);
63
+
64
+ const handleSend = () => {
65
+ if (!input.trim() && files.length === 0) return;
66
+ const text = input;
67
+ const currentFiles = files;
68
+ setInput('');
69
+ setFiles([]);
70
+
71
+ if (currentFiles.length === 0) {
72
+ sendMessage({ text });
73
+ } else {
74
+ // Build FileUIPart[] from pre-read data URLs (File[] isn't a valid type)
75
+ const fileParts = currentFiles.map((f) => ({
76
+ type: 'file',
77
+ mediaType: f.file.type || 'text/plain',
78
+ url: f.previewUrl,
79
+ filename: f.file.name,
80
+ }));
81
+ sendMessage({ text: text || undefined, files: fileParts });
82
+ }
83
+ };
84
+
85
+ const handleRetry = useCallback((message) => {
86
+ if (message.role === 'assistant') {
87
+ regenerate({ messageId: message.id });
88
+ } else {
89
+ // User message — find the next assistant message and regenerate it
90
+ const idx = messages.findIndex((m) => m.id === message.id);
91
+ const nextAssistant = messages.slice(idx + 1).find((m) => m.role === 'assistant');
92
+ if (nextAssistant) {
93
+ regenerate({ messageId: nextAssistant.id });
94
+ } else {
95
+ // No assistant response yet — extract text and resend
96
+ const text =
97
+ message.parts
98
+ ?.filter((p) => p.type === 'text')
99
+ .map((p) => p.text)
100
+ .join('\n') ||
101
+ message.content ||
102
+ '';
103
+ if (text.trim()) {
104
+ sendMessage({ text });
105
+ }
106
+ }
107
+ }
108
+ }, [messages, regenerate, sendMessage]);
109
+
110
+ const handleEdit = useCallback((message, newText) => {
111
+ const idx = messages.findIndex((m) => m.id === message.id);
112
+ if (idx === -1) return;
113
+ // Truncate conversation to before this message, then send edited text
114
+ setMessages(messages.slice(0, idx));
115
+ sendMessage({ text: newText });
116
+ }, [messages, setMessages, sendMessage]);
117
+
118
+ // Workspace is launched if containerName is set or start_coding tool was called
119
+ const isWorkspaceLaunched = !!workspace?.containerName || messages.some((m) =>
120
+ m.parts?.some((p) => p.type === 'tool-invocation' && p.toolName === 'start_coding' && p.state === 'output-available')
121
+ );
122
+
123
+ // In code mode, disable send until repo+branch selected
124
+ const codeModeCanSend = !codeMode || (!!repo && !!branch);
125
+
126
+ const codeModeToggle = (
127
+ <CodeModeToggle
128
+ enabled={codeMode}
129
+ onToggle={setCodeMode}
130
+ repo={repo}
131
+ onRepoChange={setRepo}
132
+ branch={branch}
133
+ onBranchChange={setBranch}
134
+ locked={messages.length > 0}
135
+ getRepositories={getRepositories}
136
+ getBranches={getBranches}
137
+ />
138
+ );
139
+
140
+ return (
141
+ <div className="flex h-svh flex-col">
142
+ <ChatHeader chatId={chatId} />
143
+ {messages.length === 0 ? (
144
+ <div className="flex flex-1 flex-col items-center justify-center px-4 md:px-6">
145
+ <div className="w-full max-w-4xl">
146
+ <Greeting codeMode={codeMode} />
147
+ {error && (
148
+ <div className="mt-4 rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
149
+ {error.message || 'Something went wrong. Please try again.'}
150
+ </div>
151
+ )}
152
+ <div className="mt-4">
153
+ <ChatInput
154
+ input={input}
155
+ setInput={setInput}
156
+ onSubmit={handleSend}
157
+ status={status}
158
+ stop={stop}
159
+ files={files}
160
+ setFiles={setFiles}
161
+ canSendOverride={codeModeCanSend ? undefined : false}
162
+ />
163
+ </div>
164
+ <div className="mt-5 pb-8">
165
+ {codeModeToggle}
166
+ </div>
167
+ </div>
168
+ </div>
169
+ ) : (
170
+ <>
171
+ <Messages messages={messages} status={status} onRetry={handleRetry} onEdit={handleEdit} />
172
+ {error && (
173
+ <div className="mx-auto w-full max-w-4xl px-2 md:px-4">
174
+ <div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
175
+ {error.message || 'Something went wrong. Please try again.'}
176
+ </div>
177
+ </div>
178
+ )}
179
+ <ChatInput
180
+ input={input}
181
+ setInput={setInput}
182
+ onSubmit={handleSend}
183
+ status={status}
184
+ stop={stop}
185
+ files={files}
186
+ setFiles={setFiles}
187
+ disabled={isWorkspaceLaunched}
188
+ placeholder={isWorkspaceLaunched ? 'Workspace launched — click the link above to start coding.' : 'Send a message...'}
189
+ />
190
+ {codeMode && (
191
+ <div className="mx-auto w-full max-w-4xl px-4 pb-8 md:px-6">
192
+ {codeModeToggle}
193
+ </div>
194
+ )}
195
+ </>
196
+ )}
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,367 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect, useRef } from "react";
4
+ import { PageLayout } from "./page-layout.js";
5
+ import { MessageIcon, CodeIcon, TrashIcon, SearchIcon, PlusIcon, MoreHorizontalIcon, StarIcon, StarFilledIcon, PencilIcon, ExportIcon } from "./icons.js";
6
+ import { getChats, deleteChat, renameChat, starChat, exportAllChats } from "../actions.js";
7
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "./ui/dropdown-menu.js";
8
+ import { ConfirmDialog } from "./ui/confirm-dialog.js";
9
+ import { cn } from "../utils.js";
10
+ function groupChatsByDate(chats) {
11
+ const now = /* @__PURE__ */ new Date();
12
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
13
+ const yesterday = new Date(today.getTime() - 864e5);
14
+ const last7Days = new Date(today.getTime() - 7 * 864e5);
15
+ const last30Days = new Date(today.getTime() - 30 * 864e5);
16
+ const groups = {
17
+ Starred: [],
18
+ Today: [],
19
+ Yesterday: [],
20
+ "Last 7 Days": [],
21
+ "Last 30 Days": [],
22
+ Older: []
23
+ };
24
+ for (const chat of chats) {
25
+ if (chat.starred) {
26
+ groups.Starred.push(chat);
27
+ continue;
28
+ }
29
+ const date = new Date(chat.updatedAt);
30
+ if (date >= today) {
31
+ groups.Today.push(chat);
32
+ } else if (date >= yesterday) {
33
+ groups.Yesterday.push(chat);
34
+ } else if (date >= last7Days) {
35
+ groups["Last 7 Days"].push(chat);
36
+ } else if (date >= last30Days) {
37
+ groups["Last 30 Days"].push(chat);
38
+ } else {
39
+ groups.Older.push(chat);
40
+ }
41
+ }
42
+ return groups;
43
+ }
44
+ function timeAgo(date) {
45
+ const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1e3);
46
+ if (seconds < 60) return "just now";
47
+ const minutes = Math.floor(seconds / 60);
48
+ if (minutes < 60) return `${minutes}m ago`;
49
+ const hours = Math.floor(minutes / 60);
50
+ if (hours < 24) return `${hours}h ago`;
51
+ const days = Math.floor(hours / 24);
52
+ if (days < 30) return `${days}d ago`;
53
+ const months = Math.floor(days / 30);
54
+ return `${months}mo ago`;
55
+ }
56
+ function ChatsPage({ session }) {
57
+ const [chats, setChats] = useState([]);
58
+ const [loading, setLoading] = useState(true);
59
+ const [query, setQuery] = useState("");
60
+ const [isExportingAll, setIsExportingAll] = useState(false);
61
+ const navigateToChat = (id) => {
62
+ window.location.href = id ? `/chat/${id}` : "/";
63
+ };
64
+ const loadChats = async () => {
65
+ try {
66
+ const result = await getChats();
67
+ setChats(result);
68
+ } catch (err) {
69
+ console.error("Failed to load chats:", err);
70
+ } finally {
71
+ setLoading(false);
72
+ }
73
+ };
74
+ useEffect(() => {
75
+ loadChats();
76
+ }, []);
77
+ useEffect(() => {
78
+ const handler = () => loadChats();
79
+ window.addEventListener("chatsupdated", handler);
80
+ return () => window.removeEventListener("chatsupdated", handler);
81
+ }, []);
82
+ const handleDelete = async (chatId) => {
83
+ setChats((prev) => prev.filter((c) => c.id !== chatId));
84
+ const { success } = await deleteChat(chatId);
85
+ if (!success) loadChats();
86
+ };
87
+ const handleStar = async (chatId) => {
88
+ setChats(
89
+ (prev) => prev.map((c) => c.id === chatId ? { ...c, starred: c.starred ? 0 : 1 } : c)
90
+ );
91
+ const { success } = await starChat(chatId);
92
+ if (!success) loadChats();
93
+ };
94
+ const handleExportAll = async (format) => {
95
+ if (isExportingAll) return;
96
+ setIsExportingAll(true);
97
+ try {
98
+ const exports = await exportAllChats(format);
99
+ if (!exports || exports.length === 0) return;
100
+ for (let i = 0; i < exports.length; i++) {
101
+ const { filename, content, mimeType } = exports[i];
102
+ const blob = new Blob([content], { type: mimeType });
103
+ const url = URL.createObjectURL(blob);
104
+ const a = document.createElement("a");
105
+ a.href = url;
106
+ a.download = filename;
107
+ document.body.appendChild(a);
108
+ a.click();
109
+ document.body.removeChild(a);
110
+ URL.revokeObjectURL(url);
111
+ if (i < exports.length - 1) await new Promise((r) => setTimeout(r, 120));
112
+ }
113
+ } catch (err) {
114
+ console.error("Export all failed:", err);
115
+ } finally {
116
+ setIsExportingAll(false);
117
+ }
118
+ };
119
+ const handleRename = async (chatId, title) => {
120
+ setChats(
121
+ (prev) => prev.map((c) => c.id === chatId ? { ...c, title } : c)
122
+ );
123
+ const { success } = await renameChat(chatId, title);
124
+ if (!success) loadChats();
125
+ };
126
+ const filtered = query ? chats.filter((c) => c.title?.toLowerCase().includes(query.toLowerCase())) : chats;
127
+ const grouped = groupChatsByDate(filtered);
128
+ return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
129
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-6", children: [
130
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Chats" }),
131
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
132
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
133
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
134
+ "button",
135
+ {
136
+ className: "inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium border border-input bg-background hover:bg-muted disabled:opacity-50",
137
+ disabled: isExportingAll || chats.length === 0,
138
+ "aria-label": "Export all chats",
139
+ children: [
140
+ /* @__PURE__ */ jsx(ExportIcon, { size: 14 }),
141
+ isExportingAll ? "Exporting\u2026" : "Export All"
142
+ ]
143
+ }
144
+ ) }),
145
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
146
+ /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: () => handleExportAll("md"), children: /* @__PURE__ */ jsx("span", { children: "Markdown (.md)" }) }),
147
+ /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: () => handleExportAll("txt"), children: /* @__PURE__ */ jsx("span", { children: "Plain Text (.txt)" }) }),
148
+ /* @__PURE__ */ jsx(DropdownMenuItem, { onClick: () => handleExportAll("json"), children: /* @__PURE__ */ jsx("span", { children: "JSON (.json)" }) })
149
+ ] })
150
+ ] }),
151
+ /* @__PURE__ */ jsxs(
152
+ "a",
153
+ {
154
+ href: "/",
155
+ onClick: (e) => {
156
+ e.preventDefault();
157
+ navigateToChat(null);
158
+ },
159
+ className: "inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90",
160
+ style: { textDecoration: "inherit" },
161
+ children: [
162
+ /* @__PURE__ */ jsx(PlusIcon, { size: 14 }),
163
+ "New chat"
164
+ ]
165
+ }
166
+ )
167
+ ] })
168
+ ] }),
169
+ /* @__PURE__ */ jsxs("div", { className: "relative mb-4", children: [
170
+ /* @__PURE__ */ jsx(
171
+ "input",
172
+ {
173
+ type: "text",
174
+ placeholder: "Search your chats...",
175
+ value: query,
176
+ onChange: (e) => setQuery(e.target.value),
177
+ className: "w-full rounded-md border border-input bg-background px-9 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
178
+ }
179
+ ),
180
+ /* @__PURE__ */ jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none", children: /* @__PURE__ */ jsx(SearchIcon, { size: 16 }) })
181
+ ] }),
182
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mb-4", children: [
183
+ filtered.length,
184
+ " ",
185
+ filtered.length === 1 ? "chat" : "chats"
186
+ ] }),
187
+ loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i)) }) : filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-8 text-center", children: query ? "No chats match your search." : "No chats yet. Start a conversation!" }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: Object.entries(grouped).map(
188
+ ([label, groupChats]) => groupChats.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
189
+ /* @__PURE__ */ jsx("h2", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2", children: label }),
190
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: groupChats.map((chat) => /* @__PURE__ */ jsx(
191
+ ChatRow,
192
+ {
193
+ chat,
194
+ onNavigate: navigateToChat,
195
+ onDelete: handleDelete,
196
+ onStar: handleStar,
197
+ onRename: handleRename
198
+ },
199
+ chat.id
200
+ )) })
201
+ ] }, label) : null
202
+ ) })
203
+ ] });
204
+ }
205
+ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
206
+ const [hovered, setHovered] = useState(false);
207
+ const [dropdownOpen, setDropdownOpen] = useState(false);
208
+ const [confirmDelete, setConfirmDelete] = useState(false);
209
+ const [editing, setEditing] = useState(false);
210
+ const [editTitle, setEditTitle] = useState(chat.title || "");
211
+ const inputRef = useRef(null);
212
+ const showMenu = hovered || dropdownOpen;
213
+ useEffect(() => {
214
+ if (editing && inputRef.current) {
215
+ inputRef.current.focus();
216
+ inputRef.current.select();
217
+ }
218
+ }, [editing]);
219
+ const startRename = () => {
220
+ setEditTitle(chat.title || "");
221
+ setEditing(true);
222
+ };
223
+ const saveRename = () => {
224
+ const trimmed = editTitle.trim();
225
+ if (trimmed && trimmed !== chat.title) {
226
+ onRename(chat.id, trimmed);
227
+ }
228
+ setEditing(false);
229
+ };
230
+ const cancelRename = () => {
231
+ setEditing(false);
232
+ setEditTitle(chat.title || "");
233
+ };
234
+ return /* @__PURE__ */ jsxs(
235
+ "a",
236
+ {
237
+ href: chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`,
238
+ className: "relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md",
239
+ style: { textDecoration: "inherit", color: "inherit" },
240
+ onMouseEnter: () => setHovered(true),
241
+ onMouseLeave: () => setHovered(false),
242
+ onClick: (e) => {
243
+ if (editing) {
244
+ e.preventDefault();
245
+ return;
246
+ }
247
+ e.preventDefault();
248
+ if (chat.codeWorkspaceId && chat.containerName) {
249
+ window.location.href = `/code/${chat.codeWorkspaceId}`;
250
+ } else {
251
+ onNavigate(chat.id);
252
+ }
253
+ },
254
+ children: [
255
+ chat.codeWorkspaceId && chat.containerName ? /* @__PURE__ */ jsx(CodeIcon, { size: 16 }) : /* @__PURE__ */ jsx(MessageIcon, { size: 16 }),
256
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
257
+ editing ? /* @__PURE__ */ jsx(
258
+ "input",
259
+ {
260
+ ref: inputRef,
261
+ type: "text",
262
+ value: editTitle,
263
+ onChange: (e) => setEditTitle(e.target.value),
264
+ onKeyDown: (e) => {
265
+ if (e.key === "Enter") saveRename();
266
+ if (e.key === "Escape") cancelRename();
267
+ },
268
+ onBlur: saveRename,
269
+ onClick: (e) => e.stopPropagation(),
270
+ 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"
271
+ }
272
+ ) : /* @__PURE__ */ jsx(
273
+ "span",
274
+ {
275
+ className: "text-sm truncate block",
276
+ onDoubleClick: (e) => {
277
+ e.stopPropagation();
278
+ startRename();
279
+ },
280
+ children: chat.title || "New Chat"
281
+ }
282
+ ),
283
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
284
+ "Last message ",
285
+ timeAgo(chat.updatedAt)
286
+ ] })
287
+ ] }),
288
+ !editing && /* @__PURE__ */ jsx("div", { className: cn(
289
+ "shrink-0",
290
+ showMenu ? "opacity-100" : "opacity-0 pointer-events-none"
291
+ ), children: /* @__PURE__ */ jsxs(DropdownMenu, { open: dropdownOpen, onOpenChange: setDropdownOpen, children: [
292
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
293
+ "button",
294
+ {
295
+ className: cn(
296
+ "rounded-md p-1.5",
297
+ "text-muted-foreground hover:text-foreground hover:bg-muted"
298
+ ),
299
+ "aria-label": "Chat options",
300
+ children: /* @__PURE__ */ jsx(MoreHorizontalIcon, { size: 14 })
301
+ }
302
+ ) }),
303
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", side: "bottom", children: [
304
+ /* @__PURE__ */ jsxs(
305
+ DropdownMenuItem,
306
+ {
307
+ onClick: (e) => {
308
+ e.stopPropagation();
309
+ onStar(chat.id);
310
+ },
311
+ children: [
312
+ chat.starred ? /* @__PURE__ */ jsx(StarFilledIcon, { size: 14 }) : /* @__PURE__ */ jsx(StarIcon, { size: 14 }),
313
+ chat.starred ? "Unstar" : "Star"
314
+ ]
315
+ }
316
+ ),
317
+ /* @__PURE__ */ jsxs(
318
+ DropdownMenuItem,
319
+ {
320
+ onClick: (e) => {
321
+ e.stopPropagation();
322
+ startRename();
323
+ },
324
+ children: [
325
+ /* @__PURE__ */ jsx(PencilIcon, { size: 14 }),
326
+ "Rename"
327
+ ]
328
+ }
329
+ ),
330
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
331
+ /* @__PURE__ */ jsxs(
332
+ DropdownMenuItem,
333
+ {
334
+ className: "text-destructive hover:text-destructive",
335
+ onClick: (e) => {
336
+ e.stopPropagation();
337
+ setConfirmDelete(true);
338
+ },
339
+ children: [
340
+ /* @__PURE__ */ jsx(TrashIcon, { size: 14 }),
341
+ "Delete"
342
+ ]
343
+ }
344
+ )
345
+ ] })
346
+ ] }) }),
347
+ /* @__PURE__ */ jsx(
348
+ ConfirmDialog,
349
+ {
350
+ open: confirmDelete,
351
+ title: "Delete chat?",
352
+ description: "This will permanently delete this chat and all its messages.",
353
+ confirmLabel: "Delete",
354
+ onConfirm: () => {
355
+ setConfirmDelete(false);
356
+ onDelete(chat.id);
357
+ },
358
+ onCancel: () => setConfirmDelete(false)
359
+ }
360
+ )
361
+ ]
362
+ }
363
+ );
364
+ }
365
+ export {
366
+ ChatsPage
367
+ };