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,394 @@
1
+ 'use client';
2
+
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
+
11
+ function groupChatsByDate(chats) {
12
+ const now = new Date();
13
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
14
+ const yesterday = new Date(today.getTime() - 86400000);
15
+ const last7Days = new Date(today.getTime() - 7 * 86400000);
16
+ const last30Days = new Date(today.getTime() - 30 * 86400000);
17
+
18
+ const groups = {
19
+ Starred: [],
20
+ Today: [],
21
+ Yesterday: [],
22
+ 'Last 7 Days': [],
23
+ 'Last 30 Days': [],
24
+ Older: [],
25
+ };
26
+
27
+ for (const chat of chats) {
28
+ if (chat.starred) {
29
+ groups.Starred.push(chat);
30
+ continue;
31
+ }
32
+ const date = new Date(chat.updatedAt);
33
+ if (date >= today) {
34
+ groups.Today.push(chat);
35
+ } else if (date >= yesterday) {
36
+ groups.Yesterday.push(chat);
37
+ } else if (date >= last7Days) {
38
+ groups['Last 7 Days'].push(chat);
39
+ } else if (date >= last30Days) {
40
+ groups['Last 30 Days'].push(chat);
41
+ } else {
42
+ groups.Older.push(chat);
43
+ }
44
+ }
45
+
46
+ return groups;
47
+ }
48
+
49
+ function timeAgo(date) {
50
+ const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
51
+ if (seconds < 60) return 'just now';
52
+ const minutes = Math.floor(seconds / 60);
53
+ if (minutes < 60) return `${minutes}m ago`;
54
+ const hours = Math.floor(minutes / 60);
55
+ if (hours < 24) return `${hours}h ago`;
56
+ const days = Math.floor(hours / 24);
57
+ if (days < 30) return `${days}d ago`;
58
+ const months = Math.floor(days / 30);
59
+ return `${months}mo ago`;
60
+ }
61
+
62
+ export function ChatsPage({ session }) {
63
+ const [chats, setChats] = useState([]);
64
+ const [loading, setLoading] = useState(true);
65
+ const [query, setQuery] = useState('');
66
+ const [isExportingAll, setIsExportingAll] = useState(false);
67
+
68
+ const navigateToChat = (id) => {
69
+ window.location.href = id ? `/chat/${id}` : '/';
70
+ };
71
+
72
+ const loadChats = async () => {
73
+ try {
74
+ const result = await getChats();
75
+ setChats(result);
76
+ } catch (err) {
77
+ console.error('Failed to load chats:', err);
78
+ } finally {
79
+ setLoading(false);
80
+ }
81
+ };
82
+
83
+ useEffect(() => {
84
+ loadChats();
85
+ }, []);
86
+
87
+ useEffect(() => {
88
+ const handler = () => loadChats();
89
+ window.addEventListener('chatsupdated', handler);
90
+ return () => window.removeEventListener('chatsupdated', handler);
91
+ }, []);
92
+
93
+ const handleDelete = async (chatId) => {
94
+ setChats((prev) => prev.filter((c) => c.id !== chatId));
95
+ const { success } = await deleteChat(chatId);
96
+ if (!success) loadChats();
97
+ };
98
+
99
+ const handleStar = async (chatId) => {
100
+ setChats((prev) =>
101
+ prev.map((c) => (c.id === chatId ? { ...c, starred: c.starred ? 0 : 1 } : c))
102
+ );
103
+ const { success } = await starChat(chatId);
104
+ if (!success) loadChats();
105
+ };
106
+
107
+ const handleExportAll = async (format) => {
108
+ if (isExportingAll) return;
109
+ setIsExportingAll(true);
110
+ try {
111
+ const exports = await exportAllChats(format);
112
+ if (!exports || exports.length === 0) return;
113
+ for (let i = 0; i < exports.length; i++) {
114
+ const { filename, content, mimeType } = exports[i];
115
+ const blob = new Blob([content], { type: mimeType });
116
+ const url = URL.createObjectURL(blob);
117
+ const a = document.createElement('a');
118
+ a.href = url;
119
+ a.download = filename;
120
+ document.body.appendChild(a);
121
+ a.click();
122
+ document.body.removeChild(a);
123
+ URL.revokeObjectURL(url);
124
+ if (i < exports.length - 1) await new Promise((r) => setTimeout(r, 120));
125
+ }
126
+ } catch (err) {
127
+ console.error('Export all failed:', err);
128
+ } finally {
129
+ setIsExportingAll(false);
130
+ }
131
+ };
132
+
133
+ const handleRename = async (chatId, title) => {
134
+ setChats((prev) =>
135
+ prev.map((c) => (c.id === chatId ? { ...c, title } : c))
136
+ );
137
+ const { success } = await renameChat(chatId, title);
138
+ if (!success) loadChats();
139
+ };
140
+
141
+ const filtered = query
142
+ ? chats.filter((c) => c.title?.toLowerCase().includes(query.toLowerCase()))
143
+ : chats;
144
+
145
+ const grouped = groupChatsByDate(filtered);
146
+
147
+ return (
148
+ <PageLayout session={session}>
149
+ {/* Header */}
150
+ <div className="flex items-center justify-between mb-6">
151
+ <h1 className="text-2xl font-semibold">Chats</h1>
152
+ <div className="flex items-center gap-2">
153
+ <DropdownMenu>
154
+ <DropdownMenuTrigger asChild>
155
+ <button
156
+ 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"
157
+ disabled={isExportingAll || chats.length === 0}
158
+ aria-label="Export all chats"
159
+ >
160
+ <ExportIcon size={14} />
161
+ {isExportingAll ? 'Exporting…' : 'Export All'}
162
+ </button>
163
+ </DropdownMenuTrigger>
164
+ <DropdownMenuContent align="end">
165
+ <DropdownMenuItem onClick={() => handleExportAll('md')}>
166
+ <span>Markdown (.md)</span>
167
+ </DropdownMenuItem>
168
+ <DropdownMenuItem onClick={() => handleExportAll('txt')}>
169
+ <span>Plain Text (.txt)</span>
170
+ </DropdownMenuItem>
171
+ <DropdownMenuItem onClick={() => handleExportAll('json')}>
172
+ <span>JSON (.json)</span>
173
+ </DropdownMenuItem>
174
+ </DropdownMenuContent>
175
+ </DropdownMenu>
176
+ <a
177
+ href="/"
178
+ onClick={(e) => { e.preventDefault(); navigateToChat(null); }}
179
+ 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"
180
+ style={{ textDecoration: 'inherit' }}
181
+ >
182
+ <PlusIcon size={14} />
183
+ New chat
184
+ </a>
185
+ </div>
186
+ </div>
187
+
188
+ {/* Search */}
189
+ <div className="relative mb-4">
190
+ <input
191
+ type="text"
192
+ placeholder="Search your chats..."
193
+ value={query}
194
+ onChange={(e) => setQuery(e.target.value)}
195
+ 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"
196
+ />
197
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none">
198
+ <SearchIcon size={16} />
199
+ </div>
200
+ </div>
201
+
202
+ {/* Count */}
203
+ <p className="text-sm text-muted-foreground mb-4">
204
+ {filtered.length} {filtered.length === 1 ? 'chat' : 'chats'}
205
+ </p>
206
+
207
+ {/* Chat list */}
208
+ {loading ? (
209
+ <div className="flex flex-col gap-3">
210
+ {[...Array(5)].map((_, i) => (
211
+ <div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
212
+ ))}
213
+ </div>
214
+ ) : filtered.length === 0 ? (
215
+ <p className="text-sm text-muted-foreground py-8 text-center">
216
+ {query ? 'No chats match your search.' : 'No chats yet. Start a conversation!'}
217
+ </p>
218
+ ) : (
219
+ <div className="flex flex-col">
220
+ {Object.entries(grouped).map(([label, groupChats]) =>
221
+ groupChats.length > 0 ? (
222
+ <div key={label} className="mb-4">
223
+ <h2 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
224
+ {label}
225
+ </h2>
226
+ <div className="flex flex-col divide-y divide-border">
227
+ {groupChats.map((chat) => (
228
+ <ChatRow
229
+ key={chat.id}
230
+ chat={chat}
231
+ onNavigate={navigateToChat}
232
+ onDelete={handleDelete}
233
+ onStar={handleStar}
234
+ onRename={handleRename}
235
+ />
236
+ ))}
237
+ </div>
238
+ </div>
239
+ ) : null
240
+ )}
241
+ </div>
242
+ )}
243
+ </PageLayout>
244
+ );
245
+ }
246
+
247
+ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
248
+ const [hovered, setHovered] = useState(false);
249
+ const [dropdownOpen, setDropdownOpen] = useState(false);
250
+ const [confirmDelete, setConfirmDelete] = useState(false);
251
+ const [editing, setEditing] = useState(false);
252
+ const [editTitle, setEditTitle] = useState(chat.title || '');
253
+ const inputRef = useRef(null);
254
+
255
+ const showMenu = hovered || dropdownOpen;
256
+
257
+ useEffect(() => {
258
+ if (editing && inputRef.current) {
259
+ inputRef.current.focus();
260
+ inputRef.current.select();
261
+ }
262
+ }, [editing]);
263
+
264
+ const startRename = () => {
265
+ setEditTitle(chat.title || '');
266
+ setEditing(true);
267
+ };
268
+
269
+ const saveRename = () => {
270
+ const trimmed = editTitle.trim();
271
+ if (trimmed && trimmed !== chat.title) {
272
+ onRename(chat.id, trimmed);
273
+ }
274
+ setEditing(false);
275
+ };
276
+
277
+ const cancelRename = () => {
278
+ setEditing(false);
279
+ setEditTitle(chat.title || '');
280
+ };
281
+
282
+ return (
283
+ <a
284
+ href={chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`}
285
+ className="relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md"
286
+ style={{ textDecoration: 'inherit', color: 'inherit' }}
287
+ onMouseEnter={() => setHovered(true)}
288
+ onMouseLeave={() => setHovered(false)}
289
+ onClick={(e) => {
290
+ if (editing) { e.preventDefault(); return; }
291
+ e.preventDefault();
292
+ if (chat.codeWorkspaceId && chat.containerName) {
293
+ window.location.href = `/code/${chat.codeWorkspaceId}`;
294
+ } else {
295
+ onNavigate(chat.id);
296
+ }
297
+ }}
298
+ >
299
+ {chat.codeWorkspaceId && chat.containerName ? <CodeIcon size={16} /> : <MessageIcon size={16} />}
300
+ <div className="flex-1 min-w-0">
301
+ {editing ? (
302
+ <input
303
+ ref={inputRef}
304
+ type="text"
305
+ value={editTitle}
306
+ onChange={(e) => setEditTitle(e.target.value)}
307
+ onKeyDown={(e) => {
308
+ if (e.key === 'Enter') saveRename();
309
+ if (e.key === 'Escape') cancelRename();
310
+ }}
311
+ onBlur={saveRename}
312
+ onClick={(e) => e.stopPropagation()}
313
+ 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"
314
+ />
315
+ ) : (
316
+ <span
317
+ className="text-sm truncate block"
318
+ onDoubleClick={(e) => {
319
+ e.stopPropagation();
320
+ startRename();
321
+ }}
322
+ >
323
+ {chat.title || 'New Chat'}
324
+ </span>
325
+ )}
326
+ <span className="text-xs text-muted-foreground">
327
+ Last message {timeAgo(chat.updatedAt)}
328
+ </span>
329
+ </div>
330
+ {!editing && (
331
+ <div className={cn(
332
+ 'shrink-0',
333
+ showMenu ? 'opacity-100' : 'opacity-0 pointer-events-none'
334
+ )}>
335
+ <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
336
+ <DropdownMenuTrigger asChild>
337
+ <button
338
+ className={cn(
339
+ 'rounded-md p-1.5',
340
+ 'text-muted-foreground hover:text-foreground hover:bg-muted'
341
+ )}
342
+ aria-label="Chat options"
343
+ >
344
+ <MoreHorizontalIcon size={14} />
345
+ </button>
346
+ </DropdownMenuTrigger>
347
+ <DropdownMenuContent align="end" side="bottom">
348
+ <DropdownMenuItem
349
+ onClick={(e) => {
350
+ e.stopPropagation();
351
+ onStar(chat.id);
352
+ }}
353
+ >
354
+ {chat.starred ? <StarFilledIcon size={14} /> : <StarIcon size={14} />}
355
+ {chat.starred ? 'Unstar' : 'Star'}
356
+ </DropdownMenuItem>
357
+ <DropdownMenuItem
358
+ onClick={(e) => {
359
+ e.stopPropagation();
360
+ startRename();
361
+ }}
362
+ >
363
+ <PencilIcon size={14} />
364
+ Rename
365
+ </DropdownMenuItem>
366
+ <DropdownMenuSeparator />
367
+ <DropdownMenuItem
368
+ className="text-destructive hover:text-destructive"
369
+ onClick={(e) => {
370
+ e.stopPropagation();
371
+ setConfirmDelete(true);
372
+ }}
373
+ >
374
+ <TrashIcon size={14} />
375
+ Delete
376
+ </DropdownMenuItem>
377
+ </DropdownMenuContent>
378
+ </DropdownMenu>
379
+ </div>
380
+ )}
381
+ <ConfirmDialog
382
+ open={confirmDelete}
383
+ title="Delete chat?"
384
+ description="This will permanently delete this chat and all its messages."
385
+ confirmLabel="Delete"
386
+ onConfirm={() => {
387
+ setConfirmDelete(false);
388
+ onDelete(chat.id);
389
+ }}
390
+ onCancel={() => setConfirmDelete(false)}
391
+ />
392
+ </a>
393
+ );
394
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { GitBranchIcon } from "./icons.js";
5
+ import { Combobox } from "./ui/combobox.js";
6
+ import { cn } from "../utils.js";
7
+ function CodeModeToggle({
8
+ enabled,
9
+ onToggle,
10
+ repo,
11
+ onRepoChange,
12
+ branch,
13
+ onBranchChange,
14
+ locked,
15
+ getRepositories,
16
+ getBranches
17
+ }) {
18
+ const [repos, setRepos] = useState([]);
19
+ const [branches, setBranches] = useState([]);
20
+ const [loadingRepos, setLoadingRepos] = useState(false);
21
+ const [loadingBranches, setLoadingBranches] = useState(false);
22
+ const [reposLoaded, setReposLoaded] = useState(false);
23
+ const handleToggle = useCallback(() => {
24
+ if (locked) return;
25
+ const next = !enabled;
26
+ onToggle(next);
27
+ if (next && !reposLoaded) {
28
+ setLoadingRepos(true);
29
+ getRepositories().then((data) => {
30
+ setRepos(data || []);
31
+ setReposLoaded(true);
32
+ setLoadingRepos(false);
33
+ }).catch(() => setLoadingRepos(false));
34
+ }
35
+ if (!next) {
36
+ onRepoChange("");
37
+ onBranchChange("");
38
+ setBranches([]);
39
+ }
40
+ }, [locked, enabled, reposLoaded, onToggle, onRepoChange, onBranchChange, getRepositories]);
41
+ useEffect(() => {
42
+ if (!repo || locked) return;
43
+ setLoadingBranches(true);
44
+ setBranches([]);
45
+ getBranches(repo).then((data) => {
46
+ const branchList = data || [];
47
+ setBranches(branchList);
48
+ const defaultBranch = branchList.find((b) => b.isDefault);
49
+ if (defaultBranch) {
50
+ onBranchChange(defaultBranch.name);
51
+ }
52
+ setLoadingBranches(false);
53
+ }).catch(() => setLoadingBranches(false));
54
+ }, [repo]);
55
+ if (!process.env.NEXT_PUBLIC_CODE_WORKSPACE) return null;
56
+ if (locked && enabled) {
57
+ return /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2.5 text-sm text-muted-foreground", children: [
58
+ repo && /* @__PURE__ */ jsxs(Fragment, { children: [
59
+ /* @__PURE__ */ jsx(GitBranchIcon, { size: 14 }),
60
+ /* @__PURE__ */ jsx("span", { children: repo })
61
+ ] }),
62
+ branch && /* @__PURE__ */ jsxs(Fragment, { children: [
63
+ /* @__PURE__ */ jsx("span", { className: "opacity-40", children: "\xB7" }),
64
+ /* @__PURE__ */ jsx("span", { children: branch })
65
+ ] })
66
+ ] }) });
67
+ }
68
+ const repoOptions = repos.map((r) => ({ value: r.full_name, label: r.full_name }));
69
+ const branchOptions = branches.map((b) => ({ value: b.name, label: b.name }));
70
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-3", children: [
71
+ /* @__PURE__ */ jsxs(
72
+ "button",
73
+ {
74
+ type: "button",
75
+ onClick: handleToggle,
76
+ className: "inline-flex items-center gap-2 group",
77
+ role: "switch",
78
+ "aria-checked": enabled,
79
+ "aria-label": "Toggle Code mode",
80
+ children: [
81
+ /* @__PURE__ */ jsx(
82
+ "span",
83
+ {
84
+ className: cn(
85
+ "relative inline-flex h-5 w-9 shrink-0 rounded-full transition-colors duration-200",
86
+ enabled ? "bg-primary" : "bg-muted-foreground/30"
87
+ ),
88
+ children: /* @__PURE__ */ jsx(
89
+ "span",
90
+ {
91
+ className: cn(
92
+ "absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform duration-200",
93
+ enabled && "translate-x-4"
94
+ )
95
+ }
96
+ )
97
+ }
98
+ ),
99
+ /* @__PURE__ */ jsx("span", { className: cn(
100
+ "text-xs font-medium transition-colors",
101
+ enabled ? "text-foreground" : "text-muted-foreground group-hover:text-foreground"
102
+ ), children: "Code" })
103
+ ]
104
+ }
105
+ ),
106
+ enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
107
+ /* @__PURE__ */ jsx("div", { className: "w-full sm:w-auto sm:min-w-[220px]", children: /* @__PURE__ */ jsx(
108
+ Combobox,
109
+ {
110
+ options: repoOptions,
111
+ value: repo,
112
+ onChange: onRepoChange,
113
+ placeholder: "Select repository...",
114
+ loading: loadingRepos
115
+ }
116
+ ) }),
117
+ /* @__PURE__ */ jsx("div", { className: cn("w-full sm:w-auto sm:min-w-[180px]", !repo && "opacity-50 pointer-events-none"), children: /* @__PURE__ */ jsx(
118
+ Combobox,
119
+ {
120
+ options: branchOptions,
121
+ value: branch,
122
+ onChange: onBranchChange,
123
+ placeholder: "Select branch...",
124
+ loading: loadingBranches
125
+ }
126
+ ) })
127
+ ] })
128
+ ] });
129
+ }
130
+ export {
131
+ CodeModeToggle
132
+ };
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { GitBranchIcon } from './icons.js';
5
+ import { Combobox } from './ui/combobox.js';
6
+ import { cn } from '../utils.js';
7
+
8
+ /**
9
+ * Code mode toggle with repo/branch pickers.
10
+ *
11
+ * @param {object} props
12
+ * @param {boolean} props.enabled - Whether code mode is on
13
+ * @param {Function} props.onToggle - Toggle callback
14
+ * @param {string} props.repo - Selected repo
15
+ * @param {Function} props.onRepoChange - Repo change callback
16
+ * @param {string} props.branch - Selected branch
17
+ * @param {Function} props.onBranchChange - Branch change callback
18
+ * @param {boolean} props.locked - Whether the controls are locked (after first message)
19
+ * @param {Function} props.getRepositories - Server action to fetch repos
20
+ * @param {Function} props.getBranches - Server action to fetch branches
21
+ */
22
+ export function CodeModeToggle({
23
+ enabled,
24
+ onToggle,
25
+ repo,
26
+ onRepoChange,
27
+ branch,
28
+ onBranchChange,
29
+ locked,
30
+ getRepositories,
31
+ getBranches,
32
+ }) {
33
+ const [repos, setRepos] = useState([]);
34
+ const [branches, setBranches] = useState([]);
35
+ const [loadingRepos, setLoadingRepos] = useState(false);
36
+ const [loadingBranches, setLoadingBranches] = useState(false);
37
+ const [reposLoaded, setReposLoaded] = useState(false);
38
+
39
+ // Load repos on first toggle-on
40
+ const handleToggle = useCallback(() => {
41
+ if (locked) return;
42
+ const next = !enabled;
43
+ onToggle(next);
44
+ if (next && !reposLoaded) {
45
+ setLoadingRepos(true);
46
+ getRepositories().then((data) => {
47
+ setRepos(data || []);
48
+ setReposLoaded(true);
49
+ setLoadingRepos(false);
50
+ }).catch(() => setLoadingRepos(false));
51
+ }
52
+ if (!next) {
53
+ onRepoChange('');
54
+ onBranchChange('');
55
+ setBranches([]);
56
+ }
57
+ }, [locked, enabled, reposLoaded, onToggle, onRepoChange, onBranchChange, getRepositories]);
58
+
59
+ // Load branches when repo changes
60
+ useEffect(() => {
61
+ if (!repo || locked) return;
62
+ setLoadingBranches(true);
63
+ setBranches([]);
64
+ getBranches(repo).then((data) => {
65
+ const branchList = data || [];
66
+ setBranches(branchList);
67
+ // Auto-select default branch
68
+ const defaultBranch = branchList.find((b) => b.isDefault);
69
+ if (defaultBranch) {
70
+ onBranchChange(defaultBranch.name);
71
+ }
72
+ setLoadingBranches(false);
73
+ }).catch(() => setLoadingBranches(false));
74
+ }, [repo]);
75
+
76
+ if (!process.env.NEXT_PUBLIC_CODE_WORKSPACE) return null;
77
+
78
+ // Locked mode: show as centered inline label
79
+ if (locked && enabled) {
80
+ return (
81
+ <div className="flex justify-center">
82
+ <div className="inline-flex items-center gap-2.5 text-sm text-muted-foreground">
83
+ {repo && (
84
+ <>
85
+ <GitBranchIcon size={14} />
86
+ <span>{repo}</span>
87
+ </>
88
+ )}
89
+ {branch && (
90
+ <>
91
+ <span className="opacity-40">·</span>
92
+ <span>{branch}</span>
93
+ </>
94
+ )}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ const repoOptions = repos.map((r) => ({ value: r.full_name, label: r.full_name }));
101
+ const branchOptions = branches.map((b) => ({ value: b.name, label: b.name }));
102
+
103
+ return (
104
+ <div className="flex flex-wrap items-center justify-center gap-3">
105
+ {/* Slide toggle + label */}
106
+ <button
107
+ type="button"
108
+ onClick={handleToggle}
109
+ className="inline-flex items-center gap-2 group"
110
+ role="switch"
111
+ aria-checked={enabled}
112
+ aria-label="Toggle Code mode"
113
+ >
114
+ {/* Track */}
115
+ <span
116
+ className={cn(
117
+ 'relative inline-flex h-5 w-9 shrink-0 rounded-full transition-colors duration-200',
118
+ enabled ? 'bg-primary' : 'bg-muted-foreground/30'
119
+ )}
120
+ >
121
+ {/* Knob */}
122
+ <span
123
+ className={cn(
124
+ 'absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform duration-200',
125
+ enabled && 'translate-x-4'
126
+ )}
127
+ />
128
+ </span>
129
+ {/* Label */}
130
+ <span className={cn(
131
+ 'text-xs font-medium transition-colors',
132
+ enabled ? 'text-foreground' : 'text-muted-foreground group-hover:text-foreground'
133
+ )}>
134
+ Code
135
+ </span>
136
+ </button>
137
+
138
+ {/* Repo/branch pickers — inline, both always visible */}
139
+ {enabled && (
140
+ <>
141
+ <div className="w-full sm:w-auto sm:min-w-[220px]">
142
+ <Combobox
143
+ options={repoOptions}
144
+ value={repo}
145
+ onChange={onRepoChange}
146
+ placeholder="Select repository..."
147
+ loading={loadingRepos}
148
+ />
149
+ </div>
150
+ <div className={cn("w-full sm:w-auto sm:min-w-[180px]", !repo && "opacity-50 pointer-events-none")}>
151
+ <Combobox
152
+ options={branchOptions}
153
+ value={branch}
154
+ onChange={onBranchChange}
155
+ placeholder="Select branch..."
156
+ loading={loadingBranches}
157
+ />
158
+ </div>
159
+ </>
160
+ )}
161
+ </div>
162
+ );
163
+ }