@nuasite/cms 0.20.1 → 0.20.2

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.
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "directory": "packages/astro-cms"
15
15
  },
16
16
  "license": "Apache-2.0",
17
- "version": "0.20.1",
17
+ "version": "0.20.2",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -44,6 +44,9 @@ interface ViteDevServerLike {
44
44
  on: (event: string, listener: (...args: any[]) => void) => any
45
45
  removeListener: (event: string, listener: (...args: any[]) => void) => any
46
46
  }
47
+ ws?: {
48
+ send: (payload: { type: string; path?: string }) => void
49
+ }
47
50
  }
48
51
 
49
52
  /**
@@ -111,12 +114,19 @@ export function createDevMiddleware(
111
114
 
112
115
  const route = url.replace('/_nua/cms/', '').split('?')[0]!
113
116
 
114
- handleCmsApiRoute(route, req, res, manifestWriter, config.contentDir, options.mediaAdapter).catch(
115
- (error) => {
117
+ handleCmsApiRoute(route, req, res, manifestWriter, config.contentDir, options.mediaAdapter)
118
+ .then(() => {
119
+ // Explicitly trigger full-reload after content-modifying routes.
120
+ // In sandboxed environments (e.g. E2B), chokidar file watcher events
121
+ // may not fire reliably, so we send the HMR event directly.
122
+ if (req.method === 'POST' && server.ws) {
123
+ server.ws.send({ type: 'full-reload' })
124
+ }
125
+ })
126
+ .catch((error) => {
116
127
  console.error('[astro-cms] API error:', error)
117
128
  sendError(res, 'Internal server error', 500)
118
- },
119
- )
129
+ })
120
130
  })
121
131
  }
122
132
 
package/src/editor/api.ts CHANGED
@@ -4,7 +4,6 @@ import { setAvailableTextStyles } from './text-styling'
4
4
  import type {
5
5
  CmsManifest,
6
6
  ComponentInsertOperation,
7
- DeploymentStatusResponse,
8
7
  SaveBatchRequest,
9
8
  SaveBatchResponse,
10
9
  UpdateMarkdownPageRequest,
@@ -20,68 +19,6 @@ export interface GetMarkdownContentResponse {
20
19
  filePath: string
21
20
  }
22
21
 
23
- /**
24
- * Types for CMS AI chat communication (SSE events)
25
- */
26
- export interface CmsAiChatRequest {
27
- prompt: string
28
- elementId?: string
29
- currentContent?: string
30
- pageUrl: string
31
- /** Source file context for the element being edited */
32
- context?: string
33
- sessionId?: string
34
- }
35
-
36
- export type CmsAiEvent =
37
- | CmsAiTokenEvent
38
- | CmsAiStatusEvent
39
- | CmsAiActionEvent
40
- | CmsAiErrorEvent
41
- | CmsAiDoneEvent
42
-
43
- export interface CmsAiTokenEvent {
44
- type: 'token'
45
- token: string
46
- fullText: string
47
- }
48
-
49
- export interface CmsAiStatusEvent {
50
- type: 'status'
51
- status: 'thinking' | 'coding' | 'building' | 'deploying' | 'complete'
52
- message?: string
53
- }
54
-
55
- export interface CmsAiActionEvent {
56
- type: 'action'
57
- action: CmsAiAction
58
- }
59
-
60
- export type CmsAiAction =
61
- | { name: 'refresh' }
62
- | { name: 'preview'; url: string }
63
- | { name: 'commit'; sha: string; message: string }
64
- | { name: 'apply-edit'; elementId: string; content: string; htmlContent?: string }
65
-
66
- export interface CmsAiErrorEvent {
67
- type: 'error'
68
- error: string
69
- code?: string
70
- }
71
-
72
- export interface CmsAiDoneEvent {
73
- type: 'done'
74
- summary?: string
75
- }
76
-
77
- export interface CmsAiStreamCallbacks {
78
- onToken?: (token: string, fullText: string) => void
79
- onStatus?: (status: CmsAiStatusEvent['status'], message?: string) => void
80
- onAction?: (action: CmsAiAction) => void
81
- onError?: (error: string, code?: string) => void
82
- onDone?: (summary?: string) => void
83
- }
84
-
85
22
  /**
86
23
  * Get the manifest URL for the current page
87
24
  * For example: /about -> /about.json
@@ -325,194 +262,6 @@ export async function removeComponent(
325
262
  return res.json()
326
263
  }
327
264
 
328
- /**
329
- * Parse SSE data line to event
330
- */
331
- export function parseSseEvent(data: string): CmsAiEvent | null {
332
- if (data === '[DONE]') {
333
- return { type: 'done' }
334
- }
335
- try {
336
- return JSON.parse(data) as CmsAiEvent
337
- } catch {
338
- return null
339
- }
340
- }
341
-
342
- /**
343
- * Stream AI chat response from the server using SSE
344
- * Provides rich events including status updates, actions (refresh, preview), and streaming tokens
345
- */
346
- export async function streamAiChat(
347
- apiBase: string,
348
- request: CmsAiChatRequest,
349
- callbacks: CmsAiStreamCallbacks,
350
- abortSignal?: AbortSignal,
351
- ): Promise<void> {
352
- const controller = new AbortController()
353
- const timeoutId = setTimeout(() => controller.abort(), API.AI_STREAM_TIMEOUT_MS)
354
-
355
- // Allow external abort signal to also abort the request
356
- if (abortSignal) {
357
- abortSignal.addEventListener('abort', () => controller.abort())
358
- }
359
-
360
- let doneCalled = false
361
-
362
- try {
363
- const res = await fetch(`${apiBase}/ai/chat`, {
364
- method: 'POST',
365
- credentials: 'include',
366
- headers: {
367
- 'Content-Type': 'application/json',
368
- },
369
- body: JSON.stringify(request),
370
- signal: controller.signal,
371
- })
372
-
373
- if (!res.ok) {
374
- const text = await res.text().catch(() => '')
375
- callbacks.onError?.(text || `Request failed (${res.status})`, String(res.status))
376
- return
377
- }
378
-
379
- if (!res.body) {
380
- callbacks.onError?.('No response body')
381
- return
382
- }
383
-
384
- const reader = res.body.getReader()
385
- const decoder = new TextDecoder()
386
- let buffer = ''
387
-
388
- while (true) {
389
- const { done, value } = await reader.read()
390
-
391
- if (done) {
392
- break
393
- }
394
-
395
- buffer += decoder.decode(value, { stream: true })
396
- const lines = buffer.split('\n')
397
-
398
- // Keep the last incomplete line in the buffer
399
- buffer = lines.pop() || ''
400
-
401
- for (const line of lines) {
402
- if (line.startsWith('data: ')) {
403
- const data = line.slice(6).trim()
404
- if (!data) continue
405
-
406
- const event = parseSseEvent(data)
407
- if (!event) continue
408
-
409
- switch (event.type) {
410
- case 'token':
411
- callbacks.onToken?.(event.token, event.fullText)
412
- break
413
- case 'status':
414
- callbacks.onStatus?.(event.status, event.message)
415
- break
416
- case 'action':
417
- callbacks.onAction?.(event.action)
418
- break
419
- case 'error':
420
- callbacks.onError?.(event.error, event.code)
421
- break
422
- case 'done':
423
- if (!doneCalled) {
424
- doneCalled = true
425
- callbacks.onDone?.(event.summary)
426
- }
427
- return
428
- }
429
- }
430
- }
431
- }
432
-
433
- // Process any remaining buffer
434
- if (buffer.startsWith('data: ')) {
435
- const data = buffer.slice(6).trim()
436
- if (data) {
437
- const event = parseSseEvent(data)
438
- if (event?.type === 'done' && !doneCalled) {
439
- doneCalled = true
440
- callbacks.onDone?.(event.summary)
441
- }
442
- }
443
- }
444
-
445
- if (!doneCalled) {
446
- doneCalled = true
447
- callbacks.onDone?.()
448
- }
449
- } catch (error) {
450
- if (error instanceof Error && error.name === 'AbortError') {
451
- callbacks.onError?.('Request timed out or was cancelled', 'TIMEOUT')
452
- return
453
- }
454
- callbacks.onError?.(error instanceof Error ? error.message : 'Unknown error')
455
- } finally {
456
- clearTimeout(timeoutId)
457
- }
458
- }
459
-
460
- /**
461
- * Chat message type from history endpoint
462
- */
463
- export interface ChatHistoryMessage {
464
- id: string
465
- role: 'user' | 'assistant' | 'tool'
466
- content: string | null
467
- created_at: string
468
- channel?: string
469
- identifier?: string
470
- }
471
-
472
- export interface ChatHistoryResponse {
473
- messages: ChatHistoryMessage[]
474
- hasMore: boolean
475
- }
476
-
477
- /**
478
- * Fetch AI chat history for the current project
479
- */
480
- export async function getChatHistory(
481
- apiBase: string,
482
- limit = 50,
483
- ): Promise<ChatHistoryResponse> {
484
- const res = await fetchWithTimeout(`${apiBase}/ai/chat/history?limit=${limit}`, {
485
- method: 'GET',
486
- credentials: 'include',
487
- })
488
-
489
- if (!res.ok) {
490
- const text = await res.text().catch(() => '')
491
- throw new Error(`Failed to fetch chat history (${res.status}): ${text || res.statusText}`)
492
- }
493
-
494
- return res.json()
495
- }
496
-
497
- /**
498
- * Fetch deployment status for the current project
499
- */
500
- export async function getDeploymentStatus(
501
- apiBase: string,
502
- ): Promise<DeploymentStatusResponse> {
503
- const res = await fetchWithTimeout(`${apiBase}/deployment/status`, {
504
- method: 'GET',
505
- credentials: 'include',
506
- })
507
-
508
- if (!res.ok) {
509
- const text = await res.text().catch(() => '')
510
- throw new Error(`Failed to fetch deployment status (${res.status}): ${text || res.statusText}`)
511
- }
512
-
513
- return res.json()
514
- }
515
-
516
265
  /**
517
266
  * Fetch markdown content from a file
518
267
  */
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
3
3
  import { slugify } from '../../shared'
4
4
  import { updateMarkdownPage } from '../api'
5
5
  import { STORAGE_KEYS, Z_INDEX } from '../constants'
6
- import { startDeploymentPolling } from '../editor'
7
6
  import { createMarkdownPage } from '../markdown-api'
8
7
  import {
9
8
  config,
@@ -72,15 +71,6 @@ export function MarkdownEditorOverlay() {
72
71
  }
73
72
  }, [isCreateMode, slugManuallyEdited, page?.frontmatter.title, page?.frontmatter.name])
74
73
 
75
- const handleDeploymentComplete = useCallback(
76
- (status: 'completed' | 'failed' | 'timeout') => {
77
- if (status === 'failed') {
78
- showToast('Deployment failed', 'error')
79
- }
80
- },
81
- [],
82
- )
83
-
84
74
  /** Find the [data-cms-markdown] wrapper element on the actual page (not CMS UI). */
85
75
  const findMarkdownWrapper = useCallback((): HTMLElement | null => {
86
76
  const SKIP_TAGS = new Set(['BODY', 'HTML', 'BUTTON', 'SPAN', 'A'])
@@ -133,14 +123,10 @@ export function MarkdownEditorOverlay() {
133
123
  isMarkdownPreview.value = false
134
124
  setIsSaving(false)
135
125
 
136
- showToast('Content saved, deploying...', 'success')
126
+ showToast('Content saved', 'success')
137
127
  // Clear pending entry navigation so editor doesn't auto-open after save
138
128
  sessionStorage.removeItem(STORAGE_KEYS.PENDING_ENTRY_NAVIGATION)
139
- // Close the editor immediately — the toolbar will show deploying state
140
129
  resetMarkdownEditorState()
141
- startDeploymentPolling(config.value, {
142
- onComplete: handleDeploymentComplete,
143
- })
144
130
  } else {
145
131
  showToast(result.error || 'Failed to save markdown', 'error')
146
132
  setIsSaving(false)
@@ -151,7 +137,7 @@ export function MarkdownEditorOverlay() {
151
137
  setIsSaving(false)
152
138
  }
153
139
  },
154
- [isSaving, isPreview, handleDeploymentComplete, findMarkdownWrapper],
140
+ [isSaving, isPreview, findMarkdownWrapper],
155
141
  )
156
142
 
157
143
  const handleCreate = useCallback(async () => {
@@ -207,16 +193,11 @@ export function MarkdownEditorOverlay() {
207
193
  }
208
194
  }
209
195
 
210
- showToast('Page created, deploying...', 'success')
196
+ showToast('Page created', 'success')
211
197
  resetMarkdownEditorState()
212
- startDeploymentPolling(config.value, {
213
- onComplete: (status) => {
214
- handleDeploymentComplete(status)
215
- if (status === 'completed' && redirectUrl) {
216
- startRedirectCountdown(redirectUrl, title.trim())
217
- }
218
- },
219
- })
198
+ if (redirectUrl) {
199
+ startRedirectCountdown(redirectUrl, title.trim())
200
+ }
220
201
  } else {
221
202
  showToast(result.error || 'Failed to create page', 'error')
222
203
  }
@@ -226,7 +207,7 @@ export function MarkdownEditorOverlay() {
226
207
  } finally {
227
208
  setIsSaving(false)
228
209
  }
229
- }, [isSaving, handleDeploymentComplete])
210
+ }, [isSaving])
230
211
 
231
212
  const handlePreview = useCallback(() => {
232
213
  const activeId = markdownEditorState.value.activeElementId
@@ -13,7 +13,6 @@ export interface ToolbarCallbacks {
13
13
  onDiscard: () => void
14
14
  onSelectElement?: () => void
15
15
  onMediaLibrary?: () => void
16
- onDismissDeployment?: () => void
17
16
  onNavigateChange?: () => void
18
17
  onEditContent?: () => void
19
18
  onToggleHighlights?: () => void
@@ -39,80 +38,11 @@ const GridIcon = () => (
39
38
  </svg>
40
39
  )
41
40
 
42
- const DeploymentStatusIndicator = ({ onDismiss }: { onDismiss?: () => void }) => {
43
- const deploymentStatus = signals.deploymentStatus.value
44
- const lastDeployedAt = signals.lastDeployedAt.value
45
-
46
- if (!deploymentStatus) {
47
- return null
48
- }
49
-
50
- const isActive = deploymentStatus === 'pending' || deploymentStatus === 'queued' || deploymentStatus === 'running'
51
- const isCompleted = deploymentStatus === 'completed'
52
- const isFailed = deploymentStatus === 'failed'
53
-
54
- if (!isActive && !isCompleted && !isFailed) {
55
- return null
56
- }
57
-
58
- const formatTimeAgo = (dateStr: string) => {
59
- const date = new Date(dateStr)
60
- const now = new Date()
61
- const diffMs = now.getTime() - date.getTime()
62
- const diffSec = Math.floor(diffMs / 1000)
63
- const diffMin = Math.floor(diffSec / 60)
64
-
65
- if (diffMin < 1) return 'just now'
66
- if (diffMin === 1) return '1m ago'
67
- if (diffMin < 60) return `${diffMin}m ago`
68
- const diffHour = Math.floor(diffMin / 60)
69
- if (diffHour === 1) return '1h ago'
70
- return `${diffHour}h ago`
71
- }
72
-
73
- return (
74
- <div
75
- class={cn(
76
- 'flex items-center gap-1.5 sm:gap-2 px-3 py-2 sm:px-5 sm:py-2.5 text-sm font-medium rounded-cms-pill transition-all',
77
- isActive && 'text-white/80',
78
- isCompleted && 'bg-cms-primary text-cms-primary-text',
79
- isFailed && 'bg-cms-error/20 text-cms-error cursor-pointer hover:bg-cms-error/30',
80
- )}
81
- onClick={isFailed ? onDismiss : undefined}
82
- title={isFailed ? 'Click to dismiss' : undefined}
83
- >
84
- {isActive && (
85
- <>
86
- <span class="inline-block w-3.5 h-3.5 border-2 border-white/80 border-t-transparent rounded-full animate-spin" />
87
- <span class="hidden sm:inline">Deploying</span>
88
- </>
89
- )}
90
- {isCompleted && (
91
- <>
92
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
93
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
94
- </svg>
95
- <span class="hidden sm:inline">Live{lastDeployedAt ? ` ${formatTimeAgo(lastDeployedAt)}` : ''}</span>
96
- </>
97
- )}
98
- {isFailed && (
99
- <>
100
- <svg class="w-4 h-4 sm:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
101
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
102
- </svg>
103
- <span class="hidden sm:inline">Failed</span>
104
- </>
105
- )}
106
- </div>
107
- )
108
- }
109
-
110
41
  export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
111
42
  const isEditing = signals.isEditing.value
112
43
  const showingOriginal = signals.showingOriginal.value
113
44
  const dirtyCount = signals.totalDirtyCount.value
114
45
  const isSaving = signals.isSaving.value
115
- const deploymentStatus = signals.deploymentStatus.value
116
46
  const showEditableHighlights = signals.showEditableHighlights.value
117
47
  const isPreviewingMarkdown = signals.isMarkdownPreview.value
118
48
  const currentPageCollection = signals.currentPageCollection.value
@@ -123,8 +53,6 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
123
53
 
124
54
  if (isPreviewingMarkdown) return null
125
55
 
126
- const showDeploymentStatus = deploymentStatus !== null
127
-
128
56
  const stopPropagation = (e: Event) => e.stopPropagation()
129
57
 
130
58
  const handleDiscard = async () => {
@@ -298,9 +226,6 @@ export const Toolbar = ({ callbacks, collectionDefinitions }: ToolbarProps) => {
298
226
 
299
227
  {/* Primary actions group */}
300
228
  <div class="flex items-center gap-2 sm:gap-1.5">
301
- {/* Deployment Status */}
302
- {showDeploymentStatus && <DeploymentStatusIndicator onDismiss={callbacks.onDismissDeployment} />}
303
-
304
229
  {/* Saving indicator */}
305
230
  {isSaving && !showingOriginal && (
306
231
  <div class="flex items-center gap-1.5 px-3 py-2 sm:px-5 sm:py-2.5 text-sm font-medium text-white/80">
@@ -67,8 +67,6 @@ export const LAYOUT = {
67
67
  BLOCK_EDITOR_WIDTH: 400,
68
68
  /** Block editor approximate height for positioning */
69
69
  BLOCK_EDITOR_HEIGHT: 500,
70
- /** AI chat panel width */
71
- AI_CHAT_WIDTH: 400,
72
70
  } as const
73
71
 
74
72
  /**
@@ -77,8 +75,6 @@ export const LAYOUT = {
77
75
  export const API = {
78
76
  /** Default request timeout in milliseconds */
79
77
  REQUEST_TIMEOUT_MS: 30000,
80
- /** AI streaming request timeout in milliseconds */
81
- AI_STREAM_TIMEOUT_MS: 120000,
82
78
  /** Maximum retry attempts for failed requests */
83
79
  MAX_RETRIES: 3,
84
80
  /** Base delay for exponential backoff (ms) */