deepspace 0.6.2 → 0.8.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 (63) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +22 -8
  3. package/dist/cli.js +10348 -5025
  4. package/dist/cli.js.map +1 -1
  5. package/dist/feature-installer.js +638 -0
  6. package/dist/feature-installer.js.map +1 -0
  7. package/dist/index.d.ts +229 -116
  8. package/dist/index.js +767 -788
  9. package/dist/index.js.map +1 -1
  10. package/dist/server.d.ts +403 -167
  11. package/dist/server.js +498 -367
  12. package/dist/server.js.map +1 -1
  13. package/dist/testing.d.ts +1 -1
  14. package/dist/testing.js +3 -3
  15. package/dist/testing.js.map +1 -1
  16. package/dist/worker.d.ts +541 -159
  17. package/dist/worker.js +611 -367
  18. package/dist/worker.js.map +1 -1
  19. package/features/ai-chat/feature.json +2 -0
  20. package/features/ai-chat/src/AiChatPage.tsx +2 -3
  21. package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
  22. package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
  23. package/features/ai-chat/src/ChatPanel.tsx +159 -825
  24. package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
  25. package/features/canvas/src/CanvasListPage.tsx +1 -1
  26. package/features/docs/feature.json +4 -8
  27. package/features/docs/src/DocsEditorPage.tsx +33 -326
  28. package/features/docs/src/DocsListPage.tsx +33 -29
  29. package/features/docs/src/InviteDialog.tsx +11 -40
  30. package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
  31. package/features/docs/src/docs-library-types.ts +28 -3
  32. package/features/docs/src/docs-ui.css +1 -4
  33. package/features/docs/src/use-docs-presence-access.ts +330 -0
  34. package/features/file-attachments/feature.json +2 -2
  35. package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
  36. package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
  37. package/features/file-attachments/src/useFilePreview.ts +54 -68
  38. package/features/file-manager/src/FileManagerPage.tsx +1 -1
  39. package/features/integration-test/src/integration.spec.ts +1 -1
  40. package/features/items/feature.json +1 -5
  41. package/features/items/src/ItemsPage.tsx +207 -135
  42. package/features/landing/feature.json +3 -0
  43. package/features/landing/src/LandingPage.tsx +4 -18
  44. package/features/leaderboard/feature.json +1 -5
  45. package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
  46. package/features/messaging/feature.json +0 -4
  47. package/features/messaging/src/ChatMultiPage.tsx +1 -3
  48. package/features/messaging/src/ChatPage.tsx +1 -3
  49. package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
  50. package/features/search-bar/feature.json +8 -3
  51. package/features/search-bar/src/SearchOverlay.tsx +108 -252
  52. package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
  53. package/features/search-bar/src/search-model.ts +223 -0
  54. package/features/tasks/src/TasksPage.tsx +3 -3
  55. package/features/topbar/src/Topbar.tsx +0 -17
  56. package/package.json +1 -2
  57. package/features/docs/src/docs-favorites.ts +0 -18
  58. package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
  59. package/features/file-attachments/src/attachments-constants.ts +0 -9
  60. package/features/items/src/items-constants.ts +0 -10
  61. package/features/leaderboard/src/leaderboard-constants.ts +0 -21
  62. package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
  63. package/scripts/add-feature.cjs +0 -807
@@ -6,6 +6,8 @@
6
6
  "details": "ChatPanel (reusable component) and AiChatPage (full assistant page with conversation rail). Persists to ai-chats / ai-messages collections in the app's RecordRoom — no localStorage. Routes through the Vercel AI SDK proxy with Anthropic, OpenAI, and Cerebras. User-scoped via RBAC (read: 'own').",
7
7
  "files": [
8
8
  { "src": "src/ChatPanel.tsx", "dest": "src/components/ChatPanel.tsx" },
9
+ { "src": "src/ChatPanel.messages.tsx", "dest": "src/components/ChatPanel.messages.tsx" },
10
+ { "src": "src/ChatPanel.stream.ts", "dest": "src/components/ChatPanel.stream.ts" },
9
11
  { "src": "src/AiChatPage.tsx", "dest": "src/pages/assistant.tsx" },
10
12
  { "src": "src/ai-chat-schema.ts", "dest": "src/schemas/ai-chat-schema.ts" }
11
13
  ],
@@ -175,8 +175,8 @@ export default function AiChatPage() {
175
175
  if (token) headers.Authorization = `Bearer ${token}`
176
176
  const res = await fetch('/api/ai/chats', { method: 'POST', headers })
177
177
  if (!res.ok) throw new Error(`create chat failed: ${res.status}`)
178
- const data = (await res.json()) as { chat?: { id?: string } }
179
- if (data.chat?.id) setActiveChatId(data.chat.id)
178
+ const data = (await res.json()) as { chat?: { recordId?: string } }
179
+ if (data.chat?.recordId) setActiveChatId(data.chat.recordId)
180
180
  } catch (err) {
181
181
  console.error('[ai-chat-page] create chat failed:', err)
182
182
  setCreateError(err instanceof Error ? err.message : 'Failed to create chat')
@@ -689,4 +689,3 @@ function ResizeHandle({ onStart, dragging }: { onStart: () => void; dragging: bo
689
689
  </div>
690
690
  )
691
691
  }
692
-
@@ -0,0 +1,344 @@
1
+ import {
2
+ memo,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ type ComponentPropsWithoutRef,
7
+ } from 'react'
8
+ import ReactMarkdown, { type Components } from 'react-markdown'
9
+ import remarkGfm from 'remark-gfm'
10
+ import remarkBreaks from 'remark-breaks'
11
+ import rehypeHighlight from 'rehype-highlight'
12
+ import { Check, Copy } from 'lucide-react'
13
+ import 'highlight.js/styles/atom-one-dark.css'
14
+ import type {
15
+ MessagePart,
16
+ ToolInvocation,
17
+ } from './ChatPanel.stream'
18
+
19
+ type MessageTurnProps = {
20
+ role: 'user' | 'assistant' | 'system'
21
+ content: string
22
+ parts?: unknown[]
23
+ isStreaming: boolean
24
+ }
25
+
26
+ /** Finished turns remain memoized while the active turn streams token-by-token. */
27
+ export const MessageTurn = memo(function MessageTurn({
28
+ role,
29
+ content,
30
+ parts,
31
+ isStreaming,
32
+ }: MessageTurnProps) {
33
+ const orderedParts = useMemo(
34
+ () => normalizeMessageParts(parts, content),
35
+ [parts, content],
36
+ )
37
+
38
+ if (role === 'user') {
39
+ return (
40
+ <div className="flex justify-end">
41
+ <div className="max-w-[85%] rounded-2xl bg-muted px-4 py-2.5 leading-relaxed text-foreground whitespace-pre-wrap break-words">
42
+ {content}
43
+ </div>
44
+ </div>
45
+ )
46
+ }
47
+
48
+ if (role === 'system') {
49
+ const summary = content.replace(/^Earlier conversation summary:\n?/, '')
50
+ return (
51
+ <div className="rounded-md border border-dashed border-border bg-muted/30 px-3 py-2 text-[12px] text-muted-foreground">
52
+ <div className="mb-1 font-medium tracking-tight text-foreground/80">
53
+ Earlier conversation summary
54
+ </div>
55
+ <div className="whitespace-pre-wrap leading-relaxed">{summary}</div>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ return (
61
+ <div className="flex flex-col gap-3">
62
+ {orderedParts.map((part, index) => {
63
+ if (part.type === 'text') {
64
+ if (!part.text) return null
65
+ return <MarkdownText key={index}>{part.text}</MarkdownText>
66
+ }
67
+ return <ToolRow key={index} invocation={part.toolInvocation} />
68
+ })}
69
+ {isStreaming && <LiveIndicator />}
70
+ </div>
71
+ )
72
+ })
73
+
74
+ export function EmptyState({
75
+ prompts,
76
+ onPick,
77
+ }: {
78
+ prompts: string[]
79
+ onPick: (prompt: string) => void
80
+ }) {
81
+ return (
82
+ <div className="mx-auto flex h-full max-w-[28rem] flex-col items-start justify-center gap-4 px-2">
83
+ <div className="space-y-1">
84
+ <h2 className="text-[17px] font-medium tracking-tight text-foreground">
85
+ How can I help?
86
+ </h2>
87
+ <p className="text-[13px] leading-relaxed text-muted-foreground">
88
+ I can inspect collections and help update records using your permissions.
89
+ </p>
90
+ </div>
91
+ <div className="flex flex-col gap-0.5 pt-2">
92
+ <div className="pb-1 text-[10.5px] font-medium tracking-[0.08em] uppercase text-muted-foreground">
93
+ Try
94
+ </div>
95
+ {prompts.map((prompt) => (
96
+ <button
97
+ key={prompt}
98
+ type="button"
99
+ onClick={() => onPick(prompt)}
100
+ className="group flex items-center gap-2 py-1 text-left text-[13px] text-muted-foreground transition-colors hover:text-foreground"
101
+ >
102
+ <span className="h-px w-3 shrink-0 bg-border transition-colors group-hover:bg-muted-foreground" />
103
+ <span className="truncate">{prompt}</span>
104
+ </button>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ )
109
+ }
110
+
111
+ export function ThinkingIndicator() {
112
+ return (
113
+ <div className="flex items-center gap-2 text-[13px] text-muted-foreground">
114
+ <EllipsisDots />
115
+ <span>Thinking</span>
116
+ </div>
117
+ )
118
+ }
119
+
120
+ function normalizeMessageParts(parts: unknown[] | undefined, content: string): MessagePart[] {
121
+ const validParts = Array.isArray(parts) ? parts.filter(isMessagePart) : []
122
+ if (validParts.length === 0) {
123
+ return content ? [{ type: 'text', text: content }] : []
124
+ }
125
+ if (validParts.some((part) => part.type === 'text') || !content) return validParts
126
+ return [...validParts, { type: 'text', text: content }]
127
+ }
128
+
129
+ function isMessagePart(value: unknown): value is MessagePart {
130
+ if (!value || typeof value !== 'object') return false
131
+ const candidate = value as Record<string, unknown>
132
+ if (candidate.type === 'text') return typeof candidate.text === 'string'
133
+ if (candidate.type !== 'tool-invocation') return false
134
+ return (
135
+ typeof candidate.toolCallId === 'string' &&
136
+ isToolInvocation(candidate.toolInvocation)
137
+ )
138
+ }
139
+
140
+ function isToolInvocation(value: unknown): value is ToolInvocation {
141
+ if (!value || typeof value !== 'object') return false
142
+ const candidate = value as Record<string, unknown>
143
+ return (
144
+ typeof candidate.toolName === 'string' &&
145
+ (candidate.state === 'call' || candidate.state === 'result')
146
+ )
147
+ }
148
+
149
+ function MarkdownText({ children }: { children: string }) {
150
+ return (
151
+ <div
152
+ className="text-foreground leading-relaxed
153
+ [&_p]:my-2 [&_p]:[overflow-wrap:anywhere] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0
154
+ [&_h1]:text-lg [&_h1]:font-semibold [&_h1]:my-3
155
+ [&_h2]:text-base [&_h2]:font-semibold [&_h2]:my-3
156
+ [&_h3]:font-semibold [&_h3]:my-2
157
+ [&_strong]:font-semibold [&_em]:italic
158
+ [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2
159
+ [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-6
160
+ [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-6
161
+ [&_li]:my-0.5
162
+ [&_blockquote]:my-2 [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground
163
+ [&_hr]:my-3 [&_hr]:border-border
164
+ [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-[0.9em]
165
+ [&_pre]:bg-zinc-900 [&_pre]:text-zinc-100 [&_pre]:rounded-md [&_pre]:p-3 [&_pre]:my-2 [&_pre]:overflow-x-auto
166
+ [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:rounded-none [&_pre_code]:text-inherit
167
+ [&_table]:my-2 [&_table]:w-full [&_table]:border-collapse
168
+ [&_th]:border [&_th]:border-border [&_th]:px-2 [&_th]:py-1 [&_th]:font-semibold [&_th]:text-left
169
+ [&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1"
170
+ >
171
+ <ReactMarkdown
172
+ remarkPlugins={REMARK_PLUGINS}
173
+ rehypePlugins={REHYPE_PLUGINS}
174
+ components={MARKDOWN_COMPONENTS}
175
+ >
176
+ {children}
177
+ </ReactMarkdown>
178
+ </div>
179
+ )
180
+ }
181
+
182
+ function LiveIndicator() {
183
+ return (
184
+ <div className="flex items-center gap-2 text-[12px] text-muted-foreground" aria-live="polite">
185
+ <span className="relative flex h-1.5 w-1.5">
186
+ <span className="absolute inset-0 rounded-full bg-primary/50 animate-ping" />
187
+ <span className="relative h-1.5 w-1.5 rounded-full bg-primary" />
188
+ </span>
189
+ <span className="tracking-wide">Working</span>
190
+ </div>
191
+ )
192
+ }
193
+
194
+ function ToolRow({ invocation }: { invocation: ToolInvocation }) {
195
+ const done = invocation.state === 'result'
196
+ const failed = done && isFailedResult(invocation.result)
197
+ const { label, path } = describeTool(invocation.toolName, invocation.args)
198
+
199
+ return (
200
+ <div className={`flex items-center gap-2 text-[13px] leading-tight ${done ? '' : 'animate-[pulse_2s_ease-in-out_infinite]'}`}>
201
+ <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center">
202
+ {!done
203
+ ? <Spinner />
204
+ : failed
205
+ ? <FailDot />
206
+ : <Check className="h-3 w-3 shrink-0 text-foreground/60" aria-hidden="true" />}
207
+ </span>
208
+ <span className={done ? 'text-muted-foreground' : 'text-foreground'}>{label}</span>
209
+ {path && (
210
+ <code className="truncate font-mono text-[12.5px] text-foreground/70">{path}</code>
211
+ )}
212
+ {failed && <span className="text-destructive">Failed</span>}
213
+ {!done && <EllipsisDots />}
214
+ </div>
215
+ )
216
+ }
217
+
218
+ function isFailedResult(result: unknown): boolean {
219
+ return (
220
+ typeof result === 'object' &&
221
+ result !== null &&
222
+ 'success' in result &&
223
+ result.success === false
224
+ )
225
+ }
226
+
227
+ function describeTool(
228
+ name: string,
229
+ args: unknown,
230
+ ): { label: string; path?: string } {
231
+ const values = args && typeof args === 'object'
232
+ ? args as Record<string, unknown>
233
+ : undefined
234
+ const collection = typeof values?.collection === 'string'
235
+ ? values.collection
236
+ : undefined
237
+ const recordId = typeof values?.recordId === 'string'
238
+ ? values.recordId
239
+ : undefined
240
+ const recordPath = collection && recordId
241
+ ? `${collection}/${recordId}`
242
+ : collection
243
+
244
+ switch (name) {
245
+ case 'schema_list':
246
+ return { label: 'Listing collections' }
247
+ case 'schema_describe':
248
+ return { label: 'Describing', path: collection }
249
+ case 'records_query':
250
+ return { label: 'Reading', path: collection }
251
+ case 'records_get':
252
+ return { label: 'Fetching', path: recordPath }
253
+ case 'records_create':
254
+ return { label: 'Creating record in', path: collection }
255
+ case 'records_update':
256
+ return { label: 'Updating', path: recordPath }
257
+ case 'records_delete':
258
+ return { label: 'Deleting', path: recordPath }
259
+ case 'user_current':
260
+ return { label: 'Checking current user' }
261
+ default:
262
+ return { label: 'Running', path: name }
263
+ }
264
+ }
265
+
266
+ function EllipsisDots() {
267
+ return (
268
+ <span className="inline-flex items-center gap-[3px]" aria-hidden="true">
269
+ <span className="h-1 w-1 rounded-full bg-muted-foreground animate-[pulse_1.4s_ease-in-out_0ms_infinite]" />
270
+ <span className="h-1 w-1 rounded-full bg-muted-foreground animate-[pulse_1.4s_ease-in-out_180ms_infinite]" />
271
+ <span className="h-1 w-1 rounded-full bg-muted-foreground animate-[pulse_1.4s_ease-in-out_360ms_infinite]" />
272
+ </span>
273
+ )
274
+ }
275
+
276
+ function Spinner() {
277
+ return (
278
+ <svg
279
+ width="12"
280
+ height="12"
281
+ viewBox="0 0 12 12"
282
+ className="animate-spin text-foreground/70"
283
+ aria-hidden="true"
284
+ >
285
+ <circle cx="6" cy="6" r="4.5" fill="none" stroke="currentColor" strokeOpacity="0.2" strokeWidth="1.5" />
286
+ <path d="M10.5 6 A 4.5 4.5 0 0 1 6 10.5" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
287
+ </svg>
288
+ )
289
+ }
290
+
291
+ function FailDot() {
292
+ return <span className="h-1.5 w-1.5 rounded-full bg-destructive" aria-hidden="true" />
293
+ }
294
+
295
+ type MarkdownElementProps<Tag extends 'a' | 'pre'> =
296
+ ComponentPropsWithoutRef<Tag> & { node?: unknown }
297
+
298
+ const CodeBlock = ({ children, node: _node, ...props }: MarkdownElementProps<'pre'>) => {
299
+ const ref = useRef<HTMLPreElement>(null)
300
+ const [copied, setCopied] = useState(false)
301
+
302
+ function copy() {
303
+ const text = ref.current?.textContent
304
+ if (!text) return
305
+ void navigator.clipboard.writeText(text).then(() => {
306
+ setCopied(true)
307
+ setTimeout(() => setCopied(false), 1_500)
308
+ }).catch(() => {
309
+ // Clipboard access can be denied outside a secure context.
310
+ })
311
+ }
312
+
313
+ return (
314
+ <div className="group relative">
315
+ <button
316
+ type="button"
317
+ onClick={copy}
318
+ aria-label={copied ? 'Copied' : 'Copy code'}
319
+ className="absolute right-2 top-2 inline-flex h-6 items-center gap-1 rounded-md border border-zinc-700 bg-zinc-800 px-2 text-[11px] font-medium text-zinc-200 opacity-0 transition-opacity hover:bg-zinc-700 focus-visible:opacity-100 group-hover:opacity-100"
320
+ >
321
+ {copied
322
+ ? <Check className="h-3 w-3" aria-hidden="true" />
323
+ : <Copy className="h-3 w-3" aria-hidden="true" />}
324
+ <span>{copied ? 'Copied' : 'Copy'}</span>
325
+ </button>
326
+ <pre ref={ref} {...props}>{children}</pre>
327
+ </div>
328
+ )
329
+ }
330
+
331
+ const ExternalLink = ({
332
+ children,
333
+ href,
334
+ node: _node,
335
+ ...props
336
+ }: MarkdownElementProps<'a'>) => (
337
+ <a href={href} target="_blank" rel="noopener noreferrer" {...props}>
338
+ {children}
339
+ </a>
340
+ )
341
+
342
+ const REMARK_PLUGINS = [remarkGfm, remarkBreaks]
343
+ const REHYPE_PLUGINS = [rehypeHighlight]
344
+ const MARKDOWN_COMPONENTS = { pre: CodeBlock, a: ExternalLink } satisfies Components