clawport-ui 0.8.0 → 0.8.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.
@@ -0,0 +1,77 @@
1
+ import { NextRequest } from 'next/server'
2
+ import { getMessages, appendMessages, clearConversation, validateAgentId, StoredMessage } from '@/lib/conversation-store'
3
+ import { apiErrorResponse } from '@/lib/api-error'
4
+
5
+ function isValidMessage(m: unknown): m is StoredMessage {
6
+ if (!m || typeof m !== 'object') return false
7
+ const msg = m as Record<string, unknown>
8
+ return (
9
+ typeof msg.id === 'string' && msg.id.length > 0 &&
10
+ (msg.role === 'user' || msg.role === 'assistant') &&
11
+ typeof msg.content === 'string' &&
12
+ (typeof msg.timestamp === 'number' || msg.timestamp === undefined)
13
+ )
14
+ }
15
+
16
+ export async function GET(
17
+ _req: NextRequest,
18
+ { params }: { params: Promise<{ agentId: string }> },
19
+ ) {
20
+ try {
21
+ const { agentId } = await params
22
+ validateAgentId(agentId)
23
+ const messages = getMessages(agentId)
24
+ return Response.json(messages)
25
+ } catch (err) {
26
+ if (err instanceof Error && err.message.startsWith('Invalid agent ID')) {
27
+ return Response.json({ error: err.message }, { status: 400 })
28
+ }
29
+ return apiErrorResponse(err, 'Failed to load conversation')
30
+ }
31
+ }
32
+
33
+ export async function POST(
34
+ req: NextRequest,
35
+ { params }: { params: Promise<{ agentId: string }> },
36
+ ) {
37
+ try {
38
+ const { agentId } = await params
39
+ validateAgentId(agentId)
40
+
41
+ const body = await req.json()
42
+ const messages: unknown[] = body.messages
43
+
44
+ if (!Array.isArray(messages) || messages.length === 0) {
45
+ return Response.json({ error: 'messages array required' }, { status: 400 })
46
+ }
47
+
48
+ if (!messages.every(isValidMessage)) {
49
+ return Response.json({ error: 'Invalid message format: each message needs id, role (user|assistant), and content' }, { status: 400 })
50
+ }
51
+
52
+ appendMessages(agentId, messages)
53
+ return Response.json({ ok: true })
54
+ } catch (err) {
55
+ if (err instanceof Error && err.message.startsWith('Invalid agent ID')) {
56
+ return Response.json({ error: err.message }, { status: 400 })
57
+ }
58
+ return apiErrorResponse(err, 'Failed to save conversation')
59
+ }
60
+ }
61
+
62
+ export async function DELETE(
63
+ _req: NextRequest,
64
+ { params }: { params: Promise<{ agentId: string }> },
65
+ ) {
66
+ try {
67
+ const { agentId } = await params
68
+ validateAgentId(agentId)
69
+ clearConversation(agentId)
70
+ return Response.json({ ok: true })
71
+ } catch (err) {
72
+ if (err instanceof Error && err.message.startsWith('Invalid agent ID')) {
73
+ return Response.json({ error: err.message }, { status: 400 })
74
+ }
75
+ return apiErrorResponse(err, 'Failed to clear conversation')
76
+ }
77
+ }
@@ -0,0 +1,11 @@
1
+ import { listAgentIds } from '@/lib/conversation-store'
2
+ import { apiErrorResponse } from '@/lib/api-error'
3
+
4
+ export async function GET() {
5
+ try {
6
+ const ids = listAgentIds()
7
+ return Response.json(ids)
8
+ } catch (err) {
9
+ return apiErrorResponse(err, 'Failed to list conversations')
10
+ }
11
+ }
@@ -0,0 +1,36 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { execSync } from 'child_process'
3
+ import { requireEnv } from '@/lib/env'
4
+
5
+ export async function POST() {
6
+ let bin: string
7
+ try {
8
+ bin = requireEnv('OPENCLAW_BIN')
9
+ } catch {
10
+ return NextResponse.json(
11
+ { status: 'unavailable', message: 'OPENCLAW_BIN not configured', timestamp: null },
12
+ { status: 503 }
13
+ )
14
+ }
15
+
16
+ try {
17
+ const output = execSync(`${bin} memory reindex`, {
18
+ timeout: 30000,
19
+ encoding: 'utf-8',
20
+ stdio: ['pipe', 'pipe', 'pipe'],
21
+ }).trim()
22
+
23
+ return NextResponse.json({
24
+ status: 'success',
25
+ message: output || 'Reindex completed',
26
+ timestamp: new Date().toISOString(),
27
+ })
28
+ } catch (err) {
29
+ const message = err instanceof Error ? err.message : 'Reindex failed'
30
+ return NextResponse.json({
31
+ status: 'failed',
32
+ message,
33
+ timestamp: new Date().toISOString(),
34
+ })
35
+ }
36
+ }
@@ -1,4 +1,6 @@
1
1
  import { getMemoryFiles, getMemoryConfig, getMemoryStatus, computeMemoryStats } from '@/lib/memory'
2
+ import { computeMemoryHealth } from '@/lib/memory-health'
3
+ import { writeMemoryFile, PathValidationError } from '@/lib/memory-write'
2
4
  import { apiErrorResponse } from '@/lib/api-error'
3
5
  import { NextResponse } from 'next/server'
4
6
 
@@ -8,8 +10,41 @@ export async function GET() {
8
10
  const config = getMemoryConfig()
9
11
  const status = getMemoryStatus()
10
12
  const stats = computeMemoryStats(files)
11
- return NextResponse.json({ files, config, status, stats })
13
+ const health = computeMemoryHealth(files, config, status, stats)
14
+ return NextResponse.json({ files, config, status, stats, health })
12
15
  } catch (err) {
13
16
  return apiErrorResponse(err, 'Failed to load memory files')
14
17
  }
15
18
  }
19
+
20
+ export async function PUT(req: Request) {
21
+ try {
22
+ const body = await req.json()
23
+ const { relativePath, content, expectedLastModified } = body
24
+
25
+ if (typeof content !== 'string') {
26
+ return NextResponse.json({ error: 'Content must be a string' }, { status: 400 })
27
+ }
28
+
29
+ const result = writeMemoryFile(relativePath, content, expectedLastModified)
30
+ return NextResponse.json({ ok: true, ...result })
31
+ } catch (err) {
32
+ if (err instanceof PathValidationError) {
33
+ return NextResponse.json({ error: err.message }, { status: 400 })
34
+ }
35
+ const code = (err as NodeJS.ErrnoException).code
36
+ if (code === 'ENOENT') {
37
+ return NextResponse.json({ error: 'File not found' }, { status: 404 })
38
+ }
39
+ if (code === 'E2BIG') {
40
+ return NextResponse.json({ error: (err as Error).message }, { status: 413 })
41
+ }
42
+ if (code === 'ECONFLICT') {
43
+ return NextResponse.json(
44
+ { error: 'File was modified by another process. Refresh or overwrite.' },
45
+ { status: 409 }
46
+ )
47
+ }
48
+ return apiErrorResponse(err, 'Failed to save memory file')
49
+ }
50
+ }
@@ -0,0 +1,24 @@
1
+ import { NextRequest } from 'next/server'
2
+ import { isOnboarded, setOnboarded } from '@/lib/conversation-store'
3
+ import { apiErrorResponse } from '@/lib/api-error'
4
+
5
+ export async function GET() {
6
+ try {
7
+ return Response.json({ onboarded: isOnboarded() })
8
+ } catch (err) {
9
+ return apiErrorResponse(err, 'Failed to check onboarded status')
10
+ }
11
+ }
12
+
13
+ export async function POST(req: NextRequest) {
14
+ try {
15
+ const body = await req.json()
16
+ if (typeof body.onboarded !== 'boolean') {
17
+ return Response.json({ error: 'onboarded boolean required' }, { status: 400 })
18
+ }
19
+ setOnboarded(body.onboarded)
20
+ return Response.json({ ok: true })
21
+ } catch (err) {
22
+ return apiErrorResponse(err, 'Failed to update onboarded status')
23
+ }
24
+ }
package/app/chat/page.tsx CHANGED
@@ -1,12 +1,13 @@
1
1
  'use client'
2
- import { useEffect, useState, useCallback, Suspense } from 'react'
2
+ import { useEffect, useState, useCallback, useRef, Suspense } from 'react'
3
3
  import { useSearchParams, useRouter } from 'next/navigation'
4
4
  import type { Agent } from '@/lib/types'
5
5
  import { AgentList, AgentListMobile } from '@/components/chat/AgentList'
6
6
  import { ConversationView } from '@/components/chat/ConversationView'
7
7
  import {
8
8
  loadConversations, saveConversations, getOrCreateConversation,
9
- markRead, type ConversationStore
9
+ markRead, type ConversationStore, type Message,
10
+ fetchConversation, syncToServer, fromStoredMessage,
10
11
  } from '@/lib/conversations'
11
12
 
12
13
  function MessengerApp() {
@@ -31,13 +32,72 @@ function MessengerApp() {
31
32
  setConversations(loadConversations())
32
33
  }, [])
33
34
 
34
- // Save conversations whenever they change
35
+ // Save conversations whenever they change (localStorage + server sync)
36
+ const prevConversationsRef = useRef<ConversationStore>({})
35
37
  useEffect(() => {
36
38
  if (Object.keys(conversations).length > 0) {
37
39
  saveConversations(conversations)
40
+
41
+ // Sync only new messages to server (fire-and-forget)
42
+ const prev = prevConversationsRef.current
43
+ for (const agentId of Object.keys(conversations)) {
44
+ const prevMsgs = prev[agentId]?.messages || []
45
+ const currMsgs = conversations[agentId]?.messages || []
46
+ if (currMsgs.length > prevMsgs.length) {
47
+ const prevIds = new Set(prevMsgs.map((m: Message) => m.id))
48
+ const newMsgs = currMsgs.filter((m: Message) => !prevIds.has(m.id))
49
+ if (newMsgs.length > 0) {
50
+ syncToServer(agentId, newMsgs)
51
+ }
52
+ }
53
+ }
54
+ prevConversationsRef.current = conversations
38
55
  }
39
56
  }, [conversations])
40
57
 
58
+ // Background merge: fetch server conversations and merge with localStorage
59
+ const mergedRef = useRef(false)
60
+ useEffect(() => {
61
+ if (loading || agents.length === 0 || mergedRef.current) return
62
+ mergedRef.current = true
63
+
64
+ Promise.all(
65
+ agents.map(async (agent) => {
66
+ const serverMsgs = await fetchConversation(agent.id)
67
+ return { agentId: agent.id, messages: serverMsgs }
68
+ })
69
+ ).then(results => {
70
+ setConversations(prev => {
71
+ let merged = { ...prev }
72
+ for (const { agentId, messages: serverMsgs } of results) {
73
+ if (serverMsgs.length === 0) continue
74
+ const existing = merged[agentId]
75
+ if (!existing) {
76
+ // Server has messages but localStorage doesn't — create conversation
77
+ merged[agentId] = {
78
+ agentId,
79
+ messages: serverMsgs.map(fromStoredMessage),
80
+ unread: 0,
81
+ lastActivity: serverMsgs[serverMsgs.length - 1].timestamp,
82
+ }
83
+ } else {
84
+ // Merge by message ID, sort by timestamp
85
+ const existingIds = new Set(existing.messages.map((m: Message) => m.id))
86
+ const newFromServer = serverMsgs
87
+ .filter(m => !existingIds.has(m.id))
88
+ .map(fromStoredMessage)
89
+ if (newFromServer.length > 0) {
90
+ const allMessages = [...existing.messages, ...newFromServer]
91
+ .sort((a, b) => a.timestamp - b.timestamp)
92
+ merged[agentId] = { ...existing, messages: allMessages }
93
+ }
94
+ }
95
+ }
96
+ return merged
97
+ })
98
+ })
99
+ }, [loading, agents])
100
+
41
101
  // Set default active agent on desktop only (don't auto-select on mobile)
42
102
  useEffect(() => {
43
103
  if (!loading && agents.length > 0 && !activeAgentId) {