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,249 @@
1
+ 'use client';
2
+
3
+ import { useRef, useEffect, useCallback, useState } from 'react';
4
+ import { SendIcon, StopIcon, PaperclipIcon, XIcon, FileTextIcon } from './icons.js';
5
+ import { cn } from '../utils.js';
6
+
7
+ const ACCEPTED_TYPES = [
8
+ 'image/jpeg', 'image/png', 'image/gif', 'image/webp',
9
+ 'application/pdf',
10
+ 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css',
11
+ 'text/javascript', 'text/x-python', 'text/x-typescript',
12
+ 'application/json',
13
+ ];
14
+
15
+ const MAX_FILES = 5;
16
+
17
+ function isAcceptedType(file) {
18
+ if (ACCEPTED_TYPES.includes(file.type)) return true;
19
+ // Fall back to extension for files with generic MIME types
20
+ const ext = file.name?.split('.').pop()?.toLowerCase();
21
+ const textExts = ['txt', 'md', 'csv', 'json', 'js', 'ts', 'jsx', 'tsx', 'py', 'html', 'css', 'yml', 'yaml', 'xml', 'sh', 'bash', 'rb', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'hpp'];
22
+ return textExts.includes(ext);
23
+ }
24
+
25
+ function getEffectiveType(file) {
26
+ if (ACCEPTED_TYPES.includes(file.type) && file.type !== '') return file.type;
27
+ const ext = file.name?.split('.').pop()?.toLowerCase();
28
+ const extMap = {
29
+ txt: 'text/plain', md: 'text/markdown', csv: 'text/csv',
30
+ json: 'application/json', js: 'text/javascript', ts: 'text/x-typescript',
31
+ jsx: 'text/javascript', tsx: 'text/x-typescript', py: 'text/x-python',
32
+ html: 'text/html', css: 'text/css', yml: 'text/plain', yaml: 'text/plain',
33
+ xml: 'text/plain', sh: 'text/plain', bash: 'text/plain', rb: 'text/plain',
34
+ go: 'text/plain', rs: 'text/plain', java: 'text/plain', c: 'text/plain',
35
+ cpp: 'text/plain', h: 'text/plain', hpp: 'text/plain',
36
+ };
37
+ return extMap[ext] || file.type || 'text/plain';
38
+ }
39
+
40
+ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, disabled = false, placeholder = 'Send a message...', canSendOverride }) {
41
+ const textareaRef = useRef(null);
42
+ const fileInputRef = useRef(null);
43
+ const [isDragging, setIsDragging] = useState(false);
44
+ const isStreaming = status === 'streaming' || status === 'submitted';
45
+
46
+ // Auto-resize textarea
47
+ const adjustHeight = useCallback(() => {
48
+ const textarea = textareaRef.current;
49
+ if (!textarea) return;
50
+ textarea.style.height = 'auto';
51
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
52
+ }, []);
53
+
54
+ useEffect(() => {
55
+ adjustHeight();
56
+ }, [input, adjustHeight]);
57
+
58
+ // Focus textarea on mount
59
+ useEffect(() => {
60
+ textareaRef.current?.focus();
61
+ }, []);
62
+
63
+ const handleFiles = useCallback((fileList) => {
64
+ const newFiles = Array.from(fileList).filter(isAcceptedType);
65
+ if (newFiles.length === 0) return;
66
+
67
+ // Read files outside state updater to avoid React strict mode double-invocation
68
+ newFiles.forEach((file) => {
69
+ const reader = new FileReader();
70
+ reader.onload = () => {
71
+ setFiles((current) => {
72
+ if (current.length >= MAX_FILES) return current;
73
+ return [...current, { file, previewUrl: reader.result }];
74
+ });
75
+ };
76
+ reader.readAsDataURL(file);
77
+ });
78
+ }, [setFiles]);
79
+
80
+ const removeFile = (index) => {
81
+ setFiles((prev) => prev.filter((_, i) => i !== index));
82
+ };
83
+
84
+ const handleSubmit = (e) => {
85
+ if (e) e.preventDefault();
86
+ if (disabled || (!input.trim() && files.length === 0) || isStreaming) return;
87
+ if (canSendOverride !== undefined && !canSendOverride) return;
88
+ onSubmit();
89
+ };
90
+
91
+ const handleKeyDown = (e) => {
92
+ if (e.key === 'Enter' && !e.shiftKey) {
93
+ e.preventDefault();
94
+ handleSubmit();
95
+ }
96
+ };
97
+
98
+ const handleDragOver = (e) => {
99
+ e.preventDefault();
100
+ setIsDragging(true);
101
+ };
102
+
103
+ const handleDragLeave = (e) => {
104
+ e.preventDefault();
105
+ setIsDragging(false);
106
+ };
107
+
108
+ const handleDrop = (e) => {
109
+ e.preventDefault();
110
+ setIsDragging(false);
111
+ if (e.dataTransfer?.files?.length) {
112
+ handleFiles(e.dataTransfer.files);
113
+ }
114
+ };
115
+
116
+ const canSend = canSendOverride !== undefined
117
+ ? canSendOverride && (input.trim() || files.length > 0)
118
+ : (input.trim() || files.length > 0);
119
+
120
+ // Disabled state — show locked message
121
+ if (disabled && !isStreaming) {
122
+ return (
123
+ <div className="mx-auto w-full max-w-4xl px-4 pb-4 md:px-6">
124
+ <div className="flex flex-col rounded-xl border border-border bg-muted p-2">
125
+ <div className="flex items-center px-2 py-1.5">
126
+ <span className="text-sm text-muted-foreground">{placeholder}</span>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ return (
134
+ <div className="mx-auto w-full max-w-4xl px-4 pb-4 md:px-6">
135
+ <form onSubmit={handleSubmit} className="relative">
136
+ <div
137
+ className={cn(
138
+ 'flex flex-col rounded-xl border bg-muted p-2 transition-colors',
139
+ isDragging ? 'border-primary bg-primary/5' : 'border-border'
140
+ )}
141
+ onDragOver={handleDragOver}
142
+ onDragLeave={handleDragLeave}
143
+ onDrop={handleDrop}
144
+ >
145
+ {/* File preview strip */}
146
+ {files.length > 0 && (
147
+ <div className="mb-2 flex gap-2 overflow-x-auto px-1 py-1">
148
+ {files.map((f, i) => {
149
+ const isImage = f.file.type.startsWith('image/');
150
+ return (
151
+ <div key={i} className="group relative flex-shrink-0">
152
+ {isImage ? (
153
+ <img
154
+ src={f.previewUrl}
155
+ alt={f.file.name}
156
+ className="h-16 w-16 rounded-lg object-cover"
157
+ />
158
+ ) : (
159
+ <div className="flex h-16 items-center gap-1.5 rounded-lg bg-foreground/10 px-3">
160
+ <FileTextIcon size={14} />
161
+ <span className="max-w-[100px] truncate text-xs">
162
+ {f.file.name}
163
+ </span>
164
+ </div>
165
+ )}
166
+ <button
167
+ type="button"
168
+ onClick={() => removeFile(i)}
169
+ className="absolute -right-1.5 -top-1.5 hidden rounded-full bg-foreground p-0.5 text-background group-hover:flex items-center justify-center"
170
+ aria-label={`Remove ${f.file.name}`}
171
+ >
172
+ <XIcon size={10} />
173
+ </button>
174
+ </div>
175
+ );
176
+ })}
177
+ </div>
178
+ )}
179
+
180
+ <textarea
181
+ ref={textareaRef}
182
+ value={input}
183
+ onChange={(e) => setInput(e.target.value)}
184
+ onKeyDown={handleKeyDown}
185
+ placeholder={placeholder}
186
+ rows={1}
187
+ className={cn(
188
+ 'w-full resize-none bg-transparent px-2 py-1.5 text-sm text-foreground',
189
+ 'placeholder:text-muted-foreground focus:outline-none',
190
+ 'max-h-[200px]'
191
+ )}
192
+ disabled={isStreaming}
193
+ />
194
+
195
+ <div className="flex items-center justify-between">
196
+ <div className="flex items-center gap-1">
197
+ <button
198
+ type="button"
199
+ onClick={() => fileInputRef.current?.click()}
200
+ className="inline-flex items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground"
201
+ aria-label="Attach files"
202
+ disabled={isStreaming}
203
+ >
204
+ <PaperclipIcon size={16} />
205
+ </button>
206
+
207
+ <input
208
+ ref={fileInputRef}
209
+ type="file"
210
+ multiple
211
+ accept="image/*,application/pdf,text/*,application/json,.md,.csv,.json,.js,.ts,.jsx,.tsx,.py,.html,.css,.yml,.yaml,.xml,.sh,.rb,.go,.rs,.java,.c,.cpp,.h"
212
+ className="hidden"
213
+ onChange={(e) => {
214
+ if (e.target.files?.length) handleFiles(e.target.files);
215
+ e.target.value = '';
216
+ }}
217
+ />
218
+ </div>
219
+
220
+ {isStreaming ? (
221
+ <button
222
+ type="button"
223
+ onClick={stop}
224
+ className="inline-flex items-center justify-center rounded-lg bg-foreground p-2 text-background hover:opacity-80"
225
+ aria-label="Stop generating"
226
+ >
227
+ <StopIcon size={16} />
228
+ </button>
229
+ ) : (
230
+ <button
231
+ type="submit"
232
+ disabled={!canSend}
233
+ className={cn(
234
+ 'inline-flex items-center justify-center rounded-lg p-2',
235
+ canSend
236
+ ? 'bg-foreground text-background hover:opacity-80'
237
+ : 'bg-muted-foreground/20 text-muted-foreground cursor-not-allowed'
238
+ )}
239
+ aria-label="Send message"
240
+ >
241
+ <SendIcon size={16} />
242
+ </button>
243
+ )}
244
+ </div>
245
+ </div>
246
+ </form>
247
+ </div>
248
+ );
249
+ }
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ const ChatNavContext = createContext(null);
4
+ const ChatNavProvider = ChatNavContext.Provider;
5
+ function useChatNav() {
6
+ return useContext(ChatNavContext);
7
+ }
8
+ export {
9
+ ChatNavProvider,
10
+ useChatNav
11
+ };
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+
5
+ const ChatNavContext = createContext(null);
6
+
7
+ export const ChatNavProvider = ChatNavContext.Provider;
8
+
9
+ export function useChatNav() {
10
+ return useContext(ChatNavContext);
11
+ }
@@ -0,0 +1,99 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { AppSidebar } from "./app-sidebar.js";
5
+ import { Chat } from "./chat.js";
6
+ import { SidebarProvider, SidebarInset } from "./ui/sidebar.js";
7
+ import { ChatNavProvider } from "./chat-nav-context.js";
8
+ import { getChatMessages, getChatMeta, getWorkspace } from "../actions.js";
9
+ import { v4 as uuidv4 } from "uuid";
10
+ function ChatPage({ session, needsSetup, chatId }) {
11
+ const [activeChatId, setActiveChatId] = useState(chatId || null);
12
+ const [resolvedChatId, setResolvedChatId] = useState(() => chatId ? null : uuidv4());
13
+ const [initialMessages, setInitialMessages] = useState([]);
14
+ const [workspace, setWorkspace] = useState(null);
15
+ const navigateToChat = useCallback((id) => {
16
+ if (id) {
17
+ window.history.pushState({}, "", `/chat/${id}`);
18
+ setResolvedChatId(null);
19
+ setInitialMessages([]);
20
+ setWorkspace(null);
21
+ setActiveChatId(id);
22
+ } else {
23
+ window.history.pushState({}, "", "/");
24
+ setInitialMessages([]);
25
+ setWorkspace(null);
26
+ setActiveChatId(null);
27
+ setResolvedChatId(uuidv4());
28
+ }
29
+ }, []);
30
+ useEffect(() => {
31
+ const onPopState = () => {
32
+ const match = window.location.pathname.match(/^\/chat\/(.+)/);
33
+ if (match) {
34
+ setResolvedChatId(null);
35
+ setInitialMessages([]);
36
+ setWorkspace(null);
37
+ setActiveChatId(match[1]);
38
+ } else {
39
+ setInitialMessages([]);
40
+ setWorkspace(null);
41
+ setActiveChatId(null);
42
+ setResolvedChatId(uuidv4());
43
+ }
44
+ };
45
+ window.addEventListener("popstate", onPopState);
46
+ return () => window.removeEventListener("popstate", onPopState);
47
+ }, []);
48
+ useEffect(() => {
49
+ if (activeChatId) {
50
+ getChatMessages(activeChatId).then(async (dbMessages) => {
51
+ if (dbMessages.length === 0) {
52
+ setInitialMessages([]);
53
+ setWorkspace(null);
54
+ setResolvedChatId(uuidv4());
55
+ window.history.replaceState({}, "", "/");
56
+ return;
57
+ }
58
+ const uiMessages = dbMessages.map((msg) => ({
59
+ id: msg.id,
60
+ role: msg.role,
61
+ content: msg.content,
62
+ parts: [{ type: "text", text: msg.content }],
63
+ createdAt: new Date(msg.createdAt)
64
+ }));
65
+ setInitialMessages(uiMessages);
66
+ try {
67
+ const meta = await getChatMeta(activeChatId);
68
+ if (meta?.codeWorkspaceId) {
69
+ const ws = await getWorkspace(meta.codeWorkspaceId);
70
+ setWorkspace(ws);
71
+ } else {
72
+ setWorkspace(null);
73
+ }
74
+ } catch {
75
+ setWorkspace(null);
76
+ }
77
+ setResolvedChatId(activeChatId);
78
+ });
79
+ }
80
+ }, [activeChatId]);
81
+ if (needsSetup || !session) {
82
+ return null;
83
+ }
84
+ return /* @__PURE__ */ jsx(ChatNavProvider, { value: { activeChatId: resolvedChatId, navigateToChat }, children: /* @__PURE__ */ jsxs(SidebarProvider, { children: [
85
+ /* @__PURE__ */ jsx(AppSidebar, { user: session.user }),
86
+ /* @__PURE__ */ jsx(SidebarInset, { children: resolvedChatId && /* @__PURE__ */ jsx(
87
+ Chat,
88
+ {
89
+ chatId: resolvedChatId,
90
+ initialMessages,
91
+ workspace
92
+ },
93
+ resolvedChatId
94
+ ) })
95
+ ] }) });
96
+ }
97
+ export {
98
+ ChatPage
99
+ };
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { AppSidebar } from './app-sidebar.js';
5
+ import { Chat } from './chat.js';
6
+ import { SidebarProvider, SidebarInset } from './ui/sidebar.js';
7
+ import { ChatNavProvider } from './chat-nav-context.js';
8
+ import { getChatMessages, getChatMeta, getWorkspace } from '../actions.js';
9
+ import { v4 as uuidv4 } from 'uuid';
10
+
11
+ /**
12
+ * Main chat page component.
13
+ *
14
+ * @param {object} props
15
+ * @param {object|null} props.session - Auth session (null = not logged in)
16
+ * @param {boolean} props.needsSetup - Whether setup is needed
17
+ * @param {string} [props.chatId] - Chat ID from URL (only used for initial mount)
18
+ */
19
+ export function ChatPage({ session, needsSetup, chatId }) {
20
+ const [activeChatId, setActiveChatId] = useState(chatId || null);
21
+ const [resolvedChatId, setResolvedChatId] = useState(() => chatId ? null : uuidv4());
22
+ const [initialMessages, setInitialMessages] = useState([]);
23
+ const [workspace, setWorkspace] = useState(null);
24
+
25
+ const navigateToChat = useCallback((id) => {
26
+ if (id) {
27
+ window.history.pushState({}, '', `/chat/${id}`);
28
+ setResolvedChatId(null);
29
+ setInitialMessages([]);
30
+ setWorkspace(null);
31
+ setActiveChatId(id);
32
+ } else {
33
+ window.history.pushState({}, '', '/');
34
+ setInitialMessages([]);
35
+ setWorkspace(null);
36
+ setActiveChatId(null);
37
+ setResolvedChatId(uuidv4());
38
+ }
39
+ }, []);
40
+
41
+ // Browser back/forward
42
+ useEffect(() => {
43
+ const onPopState = () => {
44
+ const match = window.location.pathname.match(/^\/chat\/(.+)/);
45
+ if (match) {
46
+ setResolvedChatId(null);
47
+ setInitialMessages([]);
48
+ setWorkspace(null);
49
+ setActiveChatId(match[1]);
50
+ } else {
51
+ setInitialMessages([]);
52
+ setWorkspace(null);
53
+ setActiveChatId(null);
54
+ setResolvedChatId(uuidv4());
55
+ }
56
+ };
57
+ window.addEventListener('popstate', onPopState);
58
+ return () => window.removeEventListener('popstate', onPopState);
59
+ }, []);
60
+
61
+ // Load messages and workspace data when activeChatId changes
62
+ useEffect(() => {
63
+ if (activeChatId) {
64
+ getChatMessages(activeChatId).then(async (dbMessages) => {
65
+ if (dbMessages.length === 0) {
66
+ // Stale chat (e.g. after login with old UUID) — start fresh
67
+ setInitialMessages([]);
68
+ setWorkspace(null);
69
+ setResolvedChatId(uuidv4());
70
+ window.history.replaceState({}, '', '/');
71
+ return;
72
+ }
73
+ const uiMessages = dbMessages.map((msg) => ({
74
+ id: msg.id,
75
+ role: msg.role,
76
+ content: msg.content,
77
+ parts: [{ type: 'text', text: msg.content }],
78
+ createdAt: new Date(msg.createdAt),
79
+ }));
80
+ setInitialMessages(uiMessages);
81
+
82
+ // Check if this is a code chat
83
+ try {
84
+ const meta = await getChatMeta(activeChatId);
85
+ if (meta?.codeWorkspaceId) {
86
+ const ws = await getWorkspace(meta.codeWorkspaceId);
87
+ setWorkspace(ws);
88
+ } else {
89
+ setWorkspace(null);
90
+ }
91
+ } catch {
92
+ setWorkspace(null);
93
+ }
94
+
95
+ setResolvedChatId(activeChatId);
96
+ });
97
+ }
98
+ }, [activeChatId]);
99
+
100
+ if (needsSetup || !session) {
101
+ return null;
102
+ }
103
+
104
+ return (
105
+ <ChatNavProvider value={{ activeChatId: resolvedChatId, navigateToChat }}>
106
+ <SidebarProvider>
107
+ <AppSidebar user={session.user} />
108
+ <SidebarInset>
109
+ {resolvedChatId && (
110
+ <Chat
111
+ key={resolvedChatId}
112
+ chatId={resolvedChatId}
113
+ initialMessages={initialMessages}
114
+ workspace={workspace}
115
+ />
116
+ )}
117
+ </SidebarInset>
118
+ </SidebarProvider>
119
+ </ChatNavProvider>
120
+ );
121
+ }
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
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
+ function Chat({ chatId, initialMessages = [], workspace = null }) {
13
+ const [input, setInput] = useState("");
14
+ const [files, setFiles] = useState([]);
15
+ const hasNavigated = useRef(false);
16
+ const [codeMode, setCodeMode] = useState(!!workspace);
17
+ const [repo, setRepo] = useState(workspace?.repo || "");
18
+ const [branch, setBranch] = useState(workspace?.branch || "");
19
+ const codeModeRef = useRef({ codeMode, repo, branch });
20
+ codeModeRef.current = { codeMode, repo, branch };
21
+ const transport = useMemo(
22
+ () => new DefaultChatTransport({
23
+ api: "/stream/chat",
24
+ body: () => ({
25
+ chatId,
26
+ ...codeModeRef.current.codeMode && codeModeRef.current.repo && codeModeRef.current.branch ? { codeMode: true, repo: codeModeRef.current.repo, branch: codeModeRef.current.branch } : {}
27
+ })
28
+ }),
29
+ [chatId]
30
+ );
31
+ const {
32
+ messages,
33
+ status,
34
+ stop,
35
+ error,
36
+ sendMessage,
37
+ regenerate,
38
+ setMessages
39
+ } = useChat({
40
+ id: chatId,
41
+ messages: initialMessages,
42
+ transport,
43
+ onError: (err) => console.error("Chat error:", err)
44
+ });
45
+ useEffect(() => {
46
+ if (!hasNavigated.current && messages.length >= 1 && status !== "ready" && window.location.pathname !== `/chat/${chatId}`) {
47
+ hasNavigated.current = true;
48
+ window.history.replaceState({}, "", `/chat/${chatId}`);
49
+ window.dispatchEvent(new Event("chatsupdated"));
50
+ setTimeout(() => window.dispatchEvent(new Event("chatsupdated")), 5e3);
51
+ }
52
+ }, [messages.length, status, chatId]);
53
+ const handleSend = () => {
54
+ if (!input.trim() && files.length === 0) return;
55
+ const text = input;
56
+ const currentFiles = files;
57
+ setInput("");
58
+ setFiles([]);
59
+ if (currentFiles.length === 0) {
60
+ sendMessage({ text });
61
+ } else {
62
+ const fileParts = currentFiles.map((f) => ({
63
+ type: "file",
64
+ mediaType: f.file.type || "text/plain",
65
+ url: f.previewUrl,
66
+ filename: f.file.name
67
+ }));
68
+ sendMessage({ text: text || void 0, files: fileParts });
69
+ }
70
+ };
71
+ const handleRetry = useCallback((message) => {
72
+ if (message.role === "assistant") {
73
+ regenerate({ messageId: message.id });
74
+ } else {
75
+ const idx = messages.findIndex((m) => m.id === message.id);
76
+ const nextAssistant = messages.slice(idx + 1).find((m) => m.role === "assistant");
77
+ if (nextAssistant) {
78
+ regenerate({ messageId: nextAssistant.id });
79
+ } else {
80
+ const text = message.parts?.filter((p) => p.type === "text").map((p) => p.text).join("\n") || message.content || "";
81
+ if (text.trim()) {
82
+ sendMessage({ text });
83
+ }
84
+ }
85
+ }
86
+ }, [messages, regenerate, sendMessage]);
87
+ const handleEdit = useCallback((message, newText) => {
88
+ const idx = messages.findIndex((m) => m.id === message.id);
89
+ if (idx === -1) return;
90
+ setMessages(messages.slice(0, idx));
91
+ sendMessage({ text: newText });
92
+ }, [messages, setMessages, sendMessage]);
93
+ const isWorkspaceLaunched = !!workspace?.containerName || messages.some(
94
+ (m) => m.parts?.some((p) => p.type === "tool-invocation" && p.toolName === "start_coding" && p.state === "output-available")
95
+ );
96
+ const codeModeCanSend = !codeMode || !!repo && !!branch;
97
+ const codeModeToggle = /* @__PURE__ */ jsx(
98
+ CodeModeToggle,
99
+ {
100
+ enabled: codeMode,
101
+ onToggle: setCodeMode,
102
+ repo,
103
+ onRepoChange: setRepo,
104
+ branch,
105
+ onBranchChange: setBranch,
106
+ locked: messages.length > 0,
107
+ getRepositories,
108
+ getBranches
109
+ }
110
+ );
111
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-svh flex-col", children: [
112
+ /* @__PURE__ */ jsx(ChatHeader, { chatId }),
113
+ messages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col items-center justify-center px-4 md:px-6", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-4xl", children: [
114
+ /* @__PURE__ */ jsx(Greeting, { codeMode }),
115
+ error && /* @__PURE__ */ jsx("div", { className: "mt-4 rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive", children: error.message || "Something went wrong. Please try again." }),
116
+ /* @__PURE__ */ jsx("div", { className: "mt-4", children: /* @__PURE__ */ jsx(
117
+ ChatInput,
118
+ {
119
+ input,
120
+ setInput,
121
+ onSubmit: handleSend,
122
+ status,
123
+ stop,
124
+ files,
125
+ setFiles,
126
+ canSendOverride: codeModeCanSend ? void 0 : false
127
+ }
128
+ ) }),
129
+ /* @__PURE__ */ jsx("div", { className: "mt-5 pb-8", children: codeModeToggle })
130
+ ] }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
131
+ /* @__PURE__ */ jsx(Messages, { messages, status, onRetry: handleRetry, onEdit: handleEdit }),
132
+ error && /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-4xl px-2 md:px-4", children: /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive", children: error.message || "Something went wrong. Please try again." }) }),
133
+ /* @__PURE__ */ jsx(
134
+ ChatInput,
135
+ {
136
+ input,
137
+ setInput,
138
+ onSubmit: handleSend,
139
+ status,
140
+ stop,
141
+ files,
142
+ setFiles,
143
+ disabled: isWorkspaceLaunched,
144
+ placeholder: isWorkspaceLaunched ? "Workspace launched \u2014 click the link above to start coding." : "Send a message..."
145
+ }
146
+ ),
147
+ codeMode && /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-4xl px-4 pb-8 md:px-6", children: codeModeToggle })
148
+ ] })
149
+ ] });
150
+ }
151
+ export {
152
+ Chat
153
+ };