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,422 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useCallback } from 'react';
4
+ import { Streamdown } from 'streamdown';
5
+ import { cn } from '../utils.js';
6
+ import { SpinnerIcon, FileTextIcon, CopyIcon, CheckIcon, RefreshIcon, SquarePenIcon, WrenchIcon, XIcon, ChevronDownIcon } from './icons.js';
7
+
8
+ function LinkSafetyModal({ url, isOpen, onClose, onConfirm }) {
9
+ const [copied, setCopied] = useState(false);
10
+
11
+ const handleCopy = useCallback(async () => {
12
+ try {
13
+ await navigator.clipboard.writeText(url);
14
+ setCopied(true);
15
+ setTimeout(() => setCopied(false), 2000);
16
+ } catch {}
17
+ }, [url]);
18
+
19
+ useEffect(() => {
20
+ if (!isOpen) return;
21
+ const onKey = (e) => { if (e.key === 'Escape') onClose(); };
22
+ document.addEventListener('keydown', onKey);
23
+ return () => document.removeEventListener('keydown', onKey);
24
+ }, [isOpen, onClose]);
25
+
26
+ if (!isOpen) return null;
27
+
28
+ return (
29
+ <div
30
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
31
+ onClick={onClose}
32
+ >
33
+ <div
34
+ className="relative mx-4 flex w-full flex-col gap-3 rounded-lg border border-border bg-background p-4 shadow-lg"
35
+ style={{ maxWidth: '340px' }}
36
+ onClick={(e) => e.stopPropagation()}
37
+ >
38
+ <div className="font-medium text-sm text-foreground">Open external link?</div>
39
+ <div className="break-all rounded bg-muted px-2.5 py-2 font-mono text-xs text-foreground">
40
+ {url}
41
+ </div>
42
+ <div className="flex gap-2">
43
+ <button
44
+ onClick={handleCopy}
45
+ className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted"
46
+ >
47
+ {copied ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
48
+ <span>{copied ? 'Copied' : 'Copy'}</span>
49
+ </button>
50
+ <button
51
+ onClick={() => { onConfirm(); onClose(); }}
52
+ className="flex flex-1 items-center justify-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90"
53
+ >
54
+ <span>Open</span>
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ export const linkSafety = {
63
+ enabled: true,
64
+ renderModal: (props) => <LinkSafetyModal {...props} />,
65
+ };
66
+
67
+ const TOOL_DISPLAY_NAMES = {
68
+ create_job: 'Create Job',
69
+ get_job_status: 'Check Job Status',
70
+ get_system_technical_specs: 'Read Tech Docs',
71
+ get_skill_building_guide: 'Read Skill Docs',
72
+ start_coding: 'Start Coding',
73
+ get_repository_details: 'Get Repository Details',
74
+ };
75
+
76
+ function getToolDisplayName(toolName) {
77
+ return TOOL_DISPLAY_NAMES[toolName] || toolName.replace(/_/g, ' ');
78
+ }
79
+
80
+ function formatContent(content) {
81
+ if (content == null) return null;
82
+ if (typeof content === 'string') {
83
+ try {
84
+ const parsed = JSON.parse(content);
85
+ return JSON.stringify(parsed, null, 2);
86
+ } catch {
87
+ return content;
88
+ }
89
+ }
90
+ return JSON.stringify(content, null, 2);
91
+ }
92
+
93
+ function ToolCall({ part }) {
94
+ const [expanded, setExpanded] = useState(false);
95
+
96
+ const toolName = part.toolName || (part.type?.startsWith('tool-') ? part.type.slice(5) : 'tool');
97
+ const displayName = getToolDisplayName(toolName);
98
+ const state = part.state || 'input-available';
99
+
100
+ const isRunning = state === 'input-streaming' || state === 'input-available';
101
+ const isDone = state === 'output-available';
102
+ const isError = state === 'output-error';
103
+
104
+ // Auto-redirect when start_coding completes successfully.
105
+ // mountedDone captures whether the tool was already finished when the component
106
+ // first rendered (i.e. the user is revisiting a chat). In that case we skip the
107
+ // redirect so they can still read the conversation.
108
+ const mountedDone = useRef(isDone);
109
+ useEffect(() => {
110
+ if (toolName !== 'start_coding' || !isDone || mountedDone.current) return;
111
+ try {
112
+ const output = typeof part.output === 'string' ? JSON.parse(part.output) : part.output;
113
+ if (output?.success && output?.workspaceUrl) {
114
+ window.location.href = output.workspaceUrl;
115
+ }
116
+ } catch {}
117
+ }, [toolName, isDone, part.output]);
118
+
119
+ return (
120
+ <div className="my-1 rounded-lg border border-border bg-background">
121
+ <button
122
+ onClick={() => setExpanded(!expanded)}
123
+ className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-muted/50 rounded-lg"
124
+ >
125
+ <WrenchIcon size={14} className="text-muted-foreground shrink-0" />
126
+ <span className="font-medium text-foreground">{displayName}</span>
127
+ <span className="ml-auto flex items-center gap-1.5 text-xs text-muted-foreground">
128
+ {isRunning && (
129
+ <>
130
+ <SpinnerIcon size={12} />
131
+ <span>Running...</span>
132
+ </>
133
+ )}
134
+ {isDone && (
135
+ <>
136
+ <CheckIcon size={12} className="text-green-500" />
137
+ <span>Done</span>
138
+ </>
139
+ )}
140
+ {isError && (
141
+ <>
142
+ <XIcon size={12} className="text-red-500" />
143
+ <span>Error</span>
144
+ </>
145
+ )}
146
+ </span>
147
+ <ChevronDownIcon
148
+ size={14}
149
+ className={cn(
150
+ 'text-muted-foreground transition-transform shrink-0',
151
+ expanded && 'rotate-180'
152
+ )}
153
+ />
154
+ </button>
155
+
156
+ {expanded && (
157
+ <div className="border-t border-border px-3 py-2 text-xs">
158
+ {part.input != null && (
159
+ <div className="mb-2">
160
+ <div className="font-medium text-muted-foreground mb-1">Input</div>
161
+ <pre className="whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto">
162
+ {formatContent(part.input)}
163
+ </pre>
164
+ </div>
165
+ )}
166
+ {part.output != null && (
167
+ <div>
168
+ <div className="font-medium text-muted-foreground mb-1">Output</div>
169
+ <pre className="whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto max-h-64 overflow-y-auto">
170
+ {formatContent(part.output)}
171
+ </pre>
172
+ </div>
173
+ )}
174
+ {part.input == null && part.output == null && (
175
+ <div className="text-muted-foreground italic">Waiting for data...</div>
176
+ )}
177
+ </div>
178
+ )}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
184
+ const isUser = message.role === 'user';
185
+ const [copied, setCopied] = useState(false);
186
+ const [editing, setEditing] = useState(false);
187
+ const [editText, setEditText] = useState('');
188
+ const textareaRef = useRef(null);
189
+
190
+ // Extract text from parts (AI SDK v5+) or fall back to content
191
+ const text =
192
+ message.parts
193
+ ?.filter((p) => p.type === 'text')
194
+ .map((p) => p.text)
195
+ .join('\n') ||
196
+ message.content ||
197
+ '';
198
+
199
+ // Extract file parts
200
+ const fileParts = message.parts?.filter((p) => p.type === 'file') || [];
201
+ const imageParts = fileParts.filter((p) => p.mediaType?.startsWith('image/'));
202
+ const otherFileParts = fileParts.filter((p) => !p.mediaType?.startsWith('image/'));
203
+ const hasToolParts = message.parts?.some((p) => p.type?.startsWith('tool-')) || false;
204
+
205
+ const handleCopy = async () => {
206
+ try {
207
+ await navigator.clipboard.writeText(text);
208
+ setCopied(true);
209
+ setTimeout(() => setCopied(false), 2000);
210
+ } catch {}
211
+ };
212
+
213
+ const handleEditStart = () => {
214
+ setEditText(text);
215
+ setEditing(true);
216
+ };
217
+
218
+ const handleEditCancel = () => {
219
+ setEditing(false);
220
+ setEditText('');
221
+ };
222
+
223
+ const handleEditSubmit = () => {
224
+ const trimmed = editText.trim();
225
+ if (trimmed && trimmed !== text) {
226
+ onEdit?.(message, trimmed);
227
+ }
228
+ setEditing(false);
229
+ setEditText('');
230
+ };
231
+
232
+ // Auto-resize and focus textarea when entering edit mode
233
+ useEffect(() => {
234
+ if (editing && textareaRef.current) {
235
+ const ta = textareaRef.current;
236
+ ta.focus();
237
+ ta.style.height = 'auto';
238
+ ta.style.height = `${ta.scrollHeight}px`;
239
+ // Move cursor to end
240
+ ta.setSelectionRange(ta.value.length, ta.value.length);
241
+ }
242
+ }, [editing]);
243
+
244
+ return (
245
+ <div
246
+ className={cn(
247
+ 'group flex gap-4 w-full',
248
+ isUser ? 'justify-end' : 'justify-start'
249
+ )}
250
+ >
251
+ <div className={cn('flex flex-col', isUser ? 'max-w-[80%]' : 'w-full')}>
252
+ {editing ? (
253
+ <div className="flex flex-col gap-2">
254
+ <textarea
255
+ ref={textareaRef}
256
+ value={editText}
257
+ onChange={(e) => {
258
+ setEditText(e.target.value);
259
+ e.target.style.height = 'auto';
260
+ e.target.style.height = `${e.target.scrollHeight}px`;
261
+ }}
262
+ onKeyDown={(e) => {
263
+ if (e.key === 'Enter' && !e.shiftKey) {
264
+ e.preventDefault();
265
+ handleEditSubmit();
266
+ }
267
+ if (e.key === 'Escape') {
268
+ handleEditCancel();
269
+ }
270
+ }}
271
+ className="w-full resize-none rounded-xl border border-border bg-muted px-4 py-3 text-sm leading-relaxed text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
272
+ rows={1}
273
+ />
274
+ <div className="flex justify-end gap-2">
275
+ <button
276
+ onClick={handleEditCancel}
277
+ className="rounded-md px-3 py-1 text-xs text-muted-foreground hover:text-foreground"
278
+ >
279
+ Cancel
280
+ </button>
281
+ <button
282
+ onClick={handleEditSubmit}
283
+ className="rounded-md bg-primary px-3 py-1 text-xs text-primary-foreground hover:opacity-80"
284
+ >
285
+ Send
286
+ </button>
287
+ </div>
288
+ </div>
289
+ ) : (
290
+ <>
291
+ <div
292
+ className={cn(
293
+ 'text-sm leading-relaxed',
294
+ isUser
295
+ ? 'rounded-xl px-4 py-3 bg-muted text-foreground'
296
+ : 'text-foreground'
297
+ )}
298
+ >
299
+ {isUser ? (
300
+ <>
301
+ {imageParts.length > 0 && (
302
+ <div className="mb-2 flex flex-wrap gap-2">
303
+ {imageParts.map((part, i) => (
304
+ <img
305
+ key={i}
306
+ src={part.url}
307
+ alt="attachment"
308
+ className="max-h-64 max-w-full rounded-lg object-contain"
309
+ />
310
+ ))}
311
+ </div>
312
+ )}
313
+ {otherFileParts.length > 0 && (
314
+ <div className="mb-2 flex flex-wrap gap-2">
315
+ {otherFileParts.map((part, i) => (
316
+ <div
317
+ key={i}
318
+ className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs bg-foreground/10"
319
+ >
320
+ <FileTextIcon size={12} />
321
+ <span className="max-w-[150px] truncate">
322
+ {part.name || part.mediaType || 'file'}
323
+ </span>
324
+ </div>
325
+ ))}
326
+ </div>
327
+ )}
328
+ {text ? (
329
+ <div className="whitespace-pre-wrap break-words">{text}</div>
330
+ ) : null}
331
+ </>
332
+ ) : (
333
+ <>
334
+ {message.parts?.length > 0 ? (
335
+ message.parts.map((part, i) => {
336
+ if (part.type === 'text') {
337
+ return <Streamdown key={i} mode={isLoading ? 'streaming' : 'static'} linkSafety={linkSafety}>{part.text}</Streamdown>;
338
+ }
339
+ if (part.type === 'file') {
340
+ if (part.mediaType?.startsWith('image/')) {
341
+ return (
342
+ <div key={i} className="mb-2">
343
+ <img src={part.url} alt="attachment" className="max-h-64 max-w-full rounded-lg object-contain" />
344
+ </div>
345
+ );
346
+ }
347
+ return (
348
+ <div key={i} className="mb-2 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs bg-foreground/10">
349
+ <FileTextIcon size={12} />
350
+ <span className="max-w-[150px] truncate">{part.name || part.mediaType || 'file'}</span>
351
+ </div>
352
+ );
353
+ }
354
+ if (part.type?.startsWith('tool-')) {
355
+ return <ToolCall key={part.toolCallId || i} part={part} />;
356
+ }
357
+ return null;
358
+ })
359
+ ) : text ? (
360
+ <Streamdown mode={isLoading ? 'streaming' : 'static'} linkSafety={linkSafety}>{text}</Streamdown>
361
+ ) : isLoading && !hasToolParts ? (
362
+ <div className="flex items-center gap-2 text-muted-foreground">
363
+ <SpinnerIcon size={14} />
364
+ <span>Working...</span>
365
+ </div>
366
+ ) : null}
367
+ </>
368
+ )}
369
+ </div>
370
+
371
+ {/* Action toolbar */}
372
+ {!isLoading && text && (
373
+ <div
374
+ className={cn(
375
+ 'flex gap-1 mt-1 opacity-0 transition-opacity group-hover:opacity-100',
376
+ isUser ? 'justify-end' : 'justify-start'
377
+ )}
378
+ >
379
+ <button
380
+ onClick={handleCopy}
381
+ className="rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted"
382
+ aria-label="Copy message"
383
+ >
384
+ {copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
385
+ </button>
386
+ {onRetry && (
387
+ <button
388
+ onClick={() => onRetry(message)}
389
+ className="rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted"
390
+ aria-label="Retry"
391
+ >
392
+ <RefreshIcon size={14} />
393
+ </button>
394
+ )}
395
+ {isUser && onEdit && (
396
+ <button
397
+ onClick={handleEditStart}
398
+ className="rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted"
399
+ aria-label="Edit message"
400
+ >
401
+ <SquarePenIcon size={14} />
402
+ </button>
403
+ )}
404
+ </div>
405
+ )}
406
+ </>
407
+ )}
408
+ </div>
409
+ </div>
410
+ );
411
+ }
412
+
413
+ export function ThinkingMessage() {
414
+ return (
415
+ <div className="flex gap-4 w-full justify-start">
416
+ <div className="flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground">
417
+ <SpinnerIcon size={14} />
418
+ <span>Thinking...</span>
419
+ </div>
420
+ </div>
421
+ );
422
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useRef, useEffect, useState } from "react";
4
+ import { PreviewMessage, ThinkingMessage } from "./message.js";
5
+ import { Greeting } from "./greeting.js";
6
+ import { ArrowDown } from "lucide-react";
7
+ function Messages({ messages, status, onRetry, onEdit }) {
8
+ const containerRef = useRef(null);
9
+ const endRef = useRef(null);
10
+ const [isAtBottom, setIsAtBottom] = useState(true);
11
+ useEffect(() => {
12
+ if (isAtBottom && endRef.current) {
13
+ endRef.current.scrollIntoView({ behavior: "smooth" });
14
+ }
15
+ }, [messages, status, isAtBottom]);
16
+ useEffect(() => {
17
+ const container = containerRef.current;
18
+ if (!container) return;
19
+ const handleScroll = () => {
20
+ const { scrollTop, scrollHeight, clientHeight } = container;
21
+ setIsAtBottom(scrollHeight - scrollTop - clientHeight < 40);
22
+ };
23
+ container.addEventListener("scroll", handleScroll, { passive: true });
24
+ return () => container.removeEventListener("scroll", handleScroll);
25
+ }, []);
26
+ const scrollToBottom = () => {
27
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
28
+ };
29
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
30
+ /* @__PURE__ */ jsx(
31
+ "div",
32
+ {
33
+ className: "absolute inset-0 touch-pan-y overflow-y-auto",
34
+ ref: containerRef,
35
+ children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-4 py-4 md:gap-6 md:px-6", children: [
36
+ messages.length === 0 && /* @__PURE__ */ jsx(Greeting, {}),
37
+ messages.map((message, index) => /* @__PURE__ */ jsx(
38
+ PreviewMessage,
39
+ {
40
+ message,
41
+ isLoading: status === "streaming" && index === messages.length - 1,
42
+ onRetry,
43
+ onEdit
44
+ },
45
+ message.id
46
+ )),
47
+ status === "submitted" && /* @__PURE__ */ jsx(ThinkingMessage, {}),
48
+ /* @__PURE__ */ jsx("div", { className: "min-h-[24px] shrink-0", ref: endRef })
49
+ ] })
50
+ }
51
+ ),
52
+ !isAtBottom && /* @__PURE__ */ jsx(
53
+ "button",
54
+ {
55
+ className: "absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background p-2 shadow-lg hover:bg-muted",
56
+ onClick: scrollToBottom,
57
+ "aria-label": "Scroll to bottom",
58
+ children: /* @__PURE__ */ jsx(ArrowDown, { className: "size-4" })
59
+ }
60
+ )
61
+ ] });
62
+ }
63
+ export {
64
+ Messages
65
+ };
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import { useRef, useEffect, useState } from 'react';
4
+ import { PreviewMessage, ThinkingMessage } from './message.js';
5
+ import { Greeting } from './greeting.js';
6
+ import { ArrowDown } from 'lucide-react';
7
+
8
+ export function Messages({ messages, status, onRetry, onEdit }) {
9
+ const containerRef = useRef(null);
10
+ const endRef = useRef(null);
11
+ const [isAtBottom, setIsAtBottom] = useState(true);
12
+
13
+ // Auto-scroll to bottom when new messages arrive
14
+ useEffect(() => {
15
+ if (isAtBottom && endRef.current) {
16
+ endRef.current.scrollIntoView({ behavior: 'smooth' });
17
+ }
18
+ }, [messages, status, isAtBottom]);
19
+
20
+ // Track scroll position
21
+ useEffect(() => {
22
+ const container = containerRef.current;
23
+ if (!container) return;
24
+
25
+ const handleScroll = () => {
26
+ const { scrollTop, scrollHeight, clientHeight } = container;
27
+ setIsAtBottom(scrollHeight - scrollTop - clientHeight < 40);
28
+ };
29
+
30
+ container.addEventListener('scroll', handleScroll, { passive: true });
31
+ return () => container.removeEventListener('scroll', handleScroll);
32
+ }, []);
33
+
34
+ const scrollToBottom = () => {
35
+ endRef.current?.scrollIntoView({ behavior: 'smooth' });
36
+ };
37
+
38
+ return (
39
+ <div className="relative flex-1">
40
+ <div
41
+ className="absolute inset-0 touch-pan-y overflow-y-auto"
42
+ ref={containerRef}
43
+ >
44
+ <div className="mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-4 py-4 md:gap-6 md:px-6">
45
+ {messages.length === 0 && <Greeting />}
46
+
47
+ {messages.map((message, index) => (
48
+ <PreviewMessage
49
+ key={message.id}
50
+ message={message}
51
+ isLoading={status === 'streaming' && index === messages.length - 1}
52
+ onRetry={onRetry}
53
+ onEdit={onEdit}
54
+ />
55
+ ))}
56
+
57
+ {status === 'submitted' && <ThinkingMessage />}
58
+
59
+ <div className="min-h-[24px] shrink-0" ref={endRef} />
60
+ </div>
61
+ </div>
62
+
63
+ {!isAtBottom && (
64
+ <button
65
+ className="absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background p-2 shadow-lg hover:bg-muted"
66
+ onClick={scrollToBottom}
67
+ aria-label="Scroll to bottom"
68
+ >
69
+ <ArrowDown className="size-4" />
70
+ </button>
71
+ )}
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { Streamdown } from "streamdown";
5
+ import { PageLayout } from "./page-layout.js";
6
+ import { BellIcon } from "./icons.js";
7
+ import { linkSafety } from "./message.js";
8
+ import { getNotifications, markNotificationsRead } from "../actions.js";
9
+ function timeAgo(ts) {
10
+ const seconds = Math.floor((Date.now() - ts) / 1e3);
11
+ if (seconds < 60) return "just now";
12
+ const minutes = Math.floor(seconds / 60);
13
+ if (minutes < 60) return `${minutes}m ago`;
14
+ const hours = Math.floor(minutes / 60);
15
+ if (hours < 24) return `${hours}h ago`;
16
+ const days = Math.floor(hours / 24);
17
+ if (days < 30) return `${days}d ago`;
18
+ const months = Math.floor(days / 30);
19
+ return `${months}mo ago`;
20
+ }
21
+ function NotificationsPage({ session }) {
22
+ const [notifications, setNotifications] = useState([]);
23
+ const [loading, setLoading] = useState(true);
24
+ useEffect(() => {
25
+ async function load() {
26
+ try {
27
+ const result = await getNotifications();
28
+ setNotifications(result);
29
+ await markNotificationsRead();
30
+ } catch (err) {
31
+ console.error("Failed to load notifications:", err);
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ }
36
+ load();
37
+ }, []);
38
+ return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
39
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-6", children: /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Notifications" }) }),
40
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mb-4", children: [
41
+ notifications.length,
42
+ " ",
43
+ notifications.length === 1 ? "notification" : "notifications"
44
+ ] }),
45
+ 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)) }) : notifications.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-8 text-center", children: "No notifications yet." }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: notifications.map((n) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 p-4 border border-border rounded-lg", children: [
46
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(BellIcon, { size: 16 }) }),
47
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
48
+ /* @__PURE__ */ jsx("div", { className: "text-sm prose-sm", children: /* @__PURE__ */ jsx(Streamdown, { mode: "static", linkSafety, children: n.notification }) }),
49
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: timeAgo(n.createdAt) })
50
+ ] })
51
+ ] }, n.id)) })
52
+ ] });
53
+ }
54
+ export {
55
+ NotificationsPage
56
+ };