@castlekit/castle 0.1.6 → 0.3.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.
- package/drizzle.config.ts +7 -0
- package/next.config.ts +1 -0
- package/package.json +20 -3
- package/src/app/api/avatars/[id]/route.ts +57 -7
- package/src/app/api/openclaw/agents/status/route.ts +55 -0
- package/src/app/api/openclaw/chat/attachments/route.ts +230 -0
- package/src/app/api/openclaw/chat/channels/route.ts +214 -0
- package/src/app/api/openclaw/chat/route.ts +272 -0
- package/src/app/api/openclaw/chat/search/route.ts +149 -0
- package/src/app/api/openclaw/chat/storage/route.ts +75 -0
- package/src/app/api/openclaw/logs/route.ts +17 -3
- package/src/app/api/openclaw/restart/route.ts +6 -1
- package/src/app/api/openclaw/session/status/route.ts +42 -0
- package/src/app/api/settings/avatar/route.ts +190 -0
- package/src/app/api/settings/route.ts +88 -0
- package/src/app/chat/[channelId]/error-boundary.tsx +64 -0
- package/src/app/chat/[channelId]/page.tsx +305 -0
- package/src/app/chat/layout.tsx +96 -0
- package/src/app/chat/page.tsx +52 -0
- package/src/app/globals.css +89 -2
- package/src/app/layout.tsx +7 -1
- package/src/app/page.tsx +49 -17
- package/src/app/settings/page.tsx +300 -0
- package/src/components/chat/agent-mention-popup.tsx +89 -0
- package/src/components/chat/archived-channels.tsx +190 -0
- package/src/components/chat/channel-list.tsx +140 -0
- package/src/components/chat/chat-input.tsx +310 -0
- package/src/components/chat/create-channel-dialog.tsx +171 -0
- package/src/components/chat/markdown-content.tsx +205 -0
- package/src/components/chat/message-bubble.tsx +152 -0
- package/src/components/chat/message-list.tsx +508 -0
- package/src/components/chat/message-queue.tsx +68 -0
- package/src/components/chat/session-divider.tsx +61 -0
- package/src/components/chat/session-stats-panel.tsx +139 -0
- package/src/components/chat/storage-indicator.tsx +76 -0
- package/src/components/layout/sidebar.tsx +126 -45
- package/src/components/layout/user-menu.tsx +29 -4
- package/src/components/providers/presence-provider.tsx +8 -0
- package/src/components/providers/search-provider.tsx +81 -0
- package/src/components/search/search-dialog.tsx +269 -0
- package/src/components/ui/avatar.tsx +11 -9
- package/src/components/ui/dialog.tsx +10 -4
- package/src/components/ui/tooltip.tsx +25 -8
- package/src/components/ui/twemoji-text.tsx +37 -0
- package/src/lib/api-security.ts +125 -0
- package/src/lib/date-utils.ts +79 -0
- package/src/lib/db/__tests__/queries.test.ts +318 -0
- package/src/lib/db/index.ts +642 -0
- package/src/lib/db/queries.ts +1017 -0
- package/src/lib/db/schema.ts +160 -0
- package/src/lib/hooks/use-agent-status.ts +251 -0
- package/src/lib/hooks/use-chat.ts +775 -0
- package/src/lib/hooks/use-openclaw.ts +105 -70
- package/src/lib/hooks/use-search.ts +113 -0
- package/src/lib/hooks/use-session-stats.ts +57 -0
- package/src/lib/hooks/use-user-settings.ts +46 -0
- package/src/lib/types/chat.ts +186 -0
- package/src/lib/types/search.ts +60 -0
- package/src/middleware.ts +52 -0
- package/vitest.config.ts +13 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import useSWR from "swr";
|
|
4
5
|
|
|
5
6
|
// ============================================================================
|
|
6
7
|
// Types
|
|
@@ -32,74 +33,89 @@ interface GatewaySSEEvent {
|
|
|
32
33
|
seq?: number;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Fetchers
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
const statusFetcher = async (url: string): Promise<OpenClawStatus> => {
|
|
41
|
+
const res = await fetch(url, { method: "POST" });
|
|
42
|
+
return res.json();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const agentsFetcher = async (url: string): Promise<OpenClawAgent[]> => {
|
|
46
|
+
const res = await fetch(url);
|
|
47
|
+
if (!res.ok) return [];
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
return data.agents || [];
|
|
50
|
+
};
|
|
51
|
+
|
|
35
52
|
// ============================================================================
|
|
36
53
|
// Hook
|
|
37
54
|
// ============================================================================
|
|
38
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Shared OpenClaw connection status and agents hook.
|
|
58
|
+
* Uses SWR for cache-based sharing across all components —
|
|
59
|
+
* any component calling useOpenClaw() gets the same cached data without extra fetches.
|
|
60
|
+
*
|
|
61
|
+
* SSE subscription pushes real-time updates that trigger SWR cache invalidation.
|
|
62
|
+
*/
|
|
39
63
|
export function useOpenClaw() {
|
|
40
|
-
const [status, setStatus] = useState<OpenClawStatus | null>(null);
|
|
41
|
-
const [agents, setAgents] = useState<OpenClawAgent[]>([]);
|
|
42
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
43
|
-
const [agentsLoading, setAgentsLoading] = useState(true);
|
|
44
64
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
45
65
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// SWR: Connection status
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
const {
|
|
70
|
+
data: status,
|
|
71
|
+
error: statusError,
|
|
72
|
+
isLoading: statusLoading,
|
|
73
|
+
mutate: mutateStatus,
|
|
74
|
+
} = useSWR<OpenClawStatus>(
|
|
75
|
+
"/api/openclaw/ping",
|
|
76
|
+
statusFetcher,
|
|
77
|
+
{
|
|
78
|
+
refreshInterval: 60000, // Background refresh every 60s
|
|
79
|
+
revalidateOnFocus: true,
|
|
80
|
+
dedupingInterval: 10000, // Dedup rapid calls within 10s
|
|
81
|
+
errorRetryCount: 2,
|
|
58
82
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const isConnected = status?.ok ?? false;
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// SWR: Agents (conditional — only fetch when connected)
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
const {
|
|
91
|
+
data: agents,
|
|
92
|
+
isLoading: agentsLoading,
|
|
93
|
+
mutate: mutateAgents,
|
|
94
|
+
} = useSWR<OpenClawAgent[]>(
|
|
95
|
+
isConnected ? "/api/openclaw/agents" : null,
|
|
96
|
+
agentsFetcher,
|
|
97
|
+
{
|
|
98
|
+
refreshInterval: 300000, // Refresh agents every 5 min
|
|
99
|
+
revalidateOnFocus: false,
|
|
100
|
+
dedupingInterval: 30000, // Dedup within 30s
|
|
76
101
|
}
|
|
77
|
-
|
|
102
|
+
);
|
|
78
103
|
|
|
79
|
-
//
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Refresh helpers
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
80
107
|
const refresh = useCallback(async () => {
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
await
|
|
108
|
+
await mutateStatus();
|
|
109
|
+
if (isConnected) {
|
|
110
|
+
await mutateAgents();
|
|
84
111
|
}
|
|
85
|
-
}, [
|
|
112
|
+
}, [mutateStatus, mutateAgents, isConnected]);
|
|
86
113
|
|
|
87
|
-
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
let cancelled = false;
|
|
90
|
-
|
|
91
|
-
async function init() {
|
|
92
|
-
const s = await fetchStatus();
|
|
93
|
-
if (!cancelled && s?.ok) {
|
|
94
|
-
await fetchAgents();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
init();
|
|
99
|
-
return () => { cancelled = true; };
|
|
100
|
-
}, [fetchStatus, fetchAgents]);
|
|
114
|
+
const refreshAgents = useCallback(() => mutateAgents(), [mutateAgents]);
|
|
101
115
|
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
102
117
|
// SSE subscription for real-time events
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
103
119
|
useEffect(() => {
|
|
104
120
|
const es = new EventSource("/api/openclaw/events");
|
|
105
121
|
eventSourceRef.current = es;
|
|
@@ -108,25 +124,39 @@ export function useOpenClaw() {
|
|
|
108
124
|
try {
|
|
109
125
|
const evt: GatewaySSEEvent = JSON.parse(e.data);
|
|
110
126
|
|
|
111
|
-
// Handle Castle state changes
|
|
127
|
+
// Handle Castle state changes — update status via SWR
|
|
112
128
|
if (evt.event === "castle.state") {
|
|
113
|
-
const payload = evt.payload as {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
const payload = evt.payload as {
|
|
130
|
+
state: string;
|
|
131
|
+
isConnected: boolean;
|
|
132
|
+
server?: Record<string, unknown>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Optimistically update the SWR cache with the SSE data
|
|
136
|
+
mutateStatus(
|
|
137
|
+
(prev) => ({
|
|
138
|
+
ok: payload.isConnected,
|
|
139
|
+
configured: prev?.configured ?? true,
|
|
140
|
+
state: payload.state,
|
|
141
|
+
server: payload.server as OpenClawStatus["server"],
|
|
142
|
+
}),
|
|
143
|
+
{ revalidate: false }
|
|
144
|
+
);
|
|
120
145
|
|
|
121
146
|
// Re-fetch agents when connection state changes to connected
|
|
122
147
|
if (payload.isConnected) {
|
|
123
|
-
|
|
148
|
+
mutateAgents();
|
|
124
149
|
}
|
|
125
150
|
}
|
|
126
151
|
|
|
127
|
-
// Handle agent-related events
|
|
152
|
+
// Handle agent-related events — re-fetch agent list
|
|
128
153
|
if (evt.event?.startsWith("agent.")) {
|
|
129
|
-
|
|
154
|
+
mutateAgents();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Handle avatar update events
|
|
158
|
+
if (evt.event === "agentAvatarUpdated") {
|
|
159
|
+
mutateAgents();
|
|
130
160
|
}
|
|
131
161
|
} catch {
|
|
132
162
|
// Ignore parse errors
|
|
@@ -135,29 +165,34 @@ export function useOpenClaw() {
|
|
|
135
165
|
|
|
136
166
|
es.onerror = () => {
|
|
137
167
|
// EventSource auto-reconnects, but update state
|
|
138
|
-
|
|
168
|
+
mutateStatus(
|
|
169
|
+
(prev) => prev ? { ...prev, ok: false, state: "disconnected" } : prev,
|
|
170
|
+
{ revalidate: false }
|
|
171
|
+
);
|
|
139
172
|
};
|
|
140
173
|
|
|
141
174
|
return () => {
|
|
142
175
|
es.close();
|
|
143
176
|
eventSourceRef.current = null;
|
|
144
177
|
};
|
|
145
|
-
}, [
|
|
178
|
+
}, [mutateStatus, mutateAgents]);
|
|
146
179
|
|
|
147
180
|
return {
|
|
148
181
|
// Status
|
|
149
182
|
status,
|
|
150
|
-
isLoading,
|
|
151
|
-
|
|
183
|
+
isLoading: statusLoading,
|
|
184
|
+
isError: !!statusError,
|
|
185
|
+
isConnected,
|
|
152
186
|
isConfigured: status?.configured ?? false,
|
|
153
187
|
latency: status?.latency_ms,
|
|
154
188
|
serverVersion: status?.server?.version,
|
|
155
189
|
|
|
156
190
|
// Agents
|
|
157
|
-
agents,
|
|
191
|
+
agents: agents ?? [],
|
|
158
192
|
agentsLoading,
|
|
159
193
|
|
|
160
194
|
// Actions
|
|
161
195
|
refresh,
|
|
196
|
+
refreshAgents,
|
|
162
197
|
};
|
|
163
198
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
|
+
import type { SearchResult } from "@/lib/types/search";
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Constants
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
const DEBOUNCE_MS = 300;
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Hook
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export function useSearch() {
|
|
17
|
+
const [query, setQuery] = useState("");
|
|
18
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
19
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
20
|
+
const [recentSearches, setRecentSearches] = useState<string[]>([]);
|
|
21
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
22
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
23
|
+
const recentLoaded = useRef(false);
|
|
24
|
+
|
|
25
|
+
// Load recent searches from DB on first mount
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (recentLoaded.current) return;
|
|
28
|
+
recentLoaded.current = true;
|
|
29
|
+
fetch("/api/openclaw/chat/search?recent=1")
|
|
30
|
+
.then((res) => (res.ok ? res.json() : { recent: [] }))
|
|
31
|
+
.then((data) => setRecentSearches(data.recent ?? []))
|
|
32
|
+
.catch(() => {});
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
// Debounced search
|
|
36
|
+
const search = useCallback((q: string) => {
|
|
37
|
+
setQuery(q);
|
|
38
|
+
|
|
39
|
+
// Cancel pending request
|
|
40
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
41
|
+
if (abortRef.current) abortRef.current.abort();
|
|
42
|
+
|
|
43
|
+
if (!q.trim()) {
|
|
44
|
+
setResults([]);
|
|
45
|
+
setIsSearching(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setIsSearching(true);
|
|
50
|
+
timerRef.current = setTimeout(async () => {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
abortRef.current = controller;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(
|
|
56
|
+
`/api/openclaw/chat/search?q=${encodeURIComponent(q.trim())}`,
|
|
57
|
+
{ signal: controller.signal }
|
|
58
|
+
);
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
const searchResults: SearchResult[] = data.results ?? [];
|
|
62
|
+
setResults(searchResults);
|
|
63
|
+
|
|
64
|
+
// Save to recent searches in DB if results found
|
|
65
|
+
if (searchResults.length > 0) {
|
|
66
|
+
const trimmed = q.trim();
|
|
67
|
+
fetch("/api/openclaw/chat/search", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({ query: trimmed }),
|
|
71
|
+
})
|
|
72
|
+
.then(() => {
|
|
73
|
+
// Update local state to reflect the save
|
|
74
|
+
setRecentSearches((prev) => {
|
|
75
|
+
const filtered = prev.filter((s) => s !== trimmed);
|
|
76
|
+
return [trimmed, ...filtered].slice(0, 15);
|
|
77
|
+
});
|
|
78
|
+
})
|
|
79
|
+
.catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if ((err as Error).name !== "AbortError") {
|
|
84
|
+
setResults([]);
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
setIsSearching(false);
|
|
88
|
+
}
|
|
89
|
+
}, DEBOUNCE_MS);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const clearRecentSearches = useCallback(() => {
|
|
93
|
+
setRecentSearches([]);
|
|
94
|
+
fetch("/api/openclaw/chat/search", { method: "DELETE" }).catch(() => {});
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
// Cleanup on unmount
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return () => {
|
|
100
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
101
|
+
if (abortRef.current) abortRef.current.abort();
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
query,
|
|
107
|
+
setQuery: search,
|
|
108
|
+
results,
|
|
109
|
+
isSearching,
|
|
110
|
+
recentSearches,
|
|
111
|
+
clearRecentSearches,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import useSWR from "swr";
|
|
5
|
+
import type { SessionStatus } from "@/lib/types/chat";
|
|
6
|
+
|
|
7
|
+
const fetcher = async (url: string): Promise<SessionStatus | null> => {
|
|
8
|
+
const res = await fetch(url);
|
|
9
|
+
if (res.status === 204) return null;
|
|
10
|
+
if (!res.ok) throw new Error("Failed to fetch session stats");
|
|
11
|
+
return res.json();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface UseSessionStatsOptions {
|
|
15
|
+
sessionKey: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UseSessionStatsReturn {
|
|
19
|
+
stats: SessionStatus | null;
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
isError: boolean;
|
|
22
|
+
refresh: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SWR-based hook for fetching session statistics from the Gateway.
|
|
27
|
+
* Conditional fetching: only when sessionKey is non-null.
|
|
28
|
+
* Polls every 30s while active, dedupes within 5s.
|
|
29
|
+
*/
|
|
30
|
+
export function useSessionStats({ sessionKey }: UseSessionStatsOptions): UseSessionStatsReturn {
|
|
31
|
+
const {
|
|
32
|
+
data,
|
|
33
|
+
isLoading,
|
|
34
|
+
error,
|
|
35
|
+
mutate,
|
|
36
|
+
} = useSWR<SessionStatus | null>(
|
|
37
|
+
sessionKey ? `/api/openclaw/session/status?sessionKey=${sessionKey}` : null,
|
|
38
|
+
fetcher,
|
|
39
|
+
{
|
|
40
|
+
refreshInterval: 30000,
|
|
41
|
+
dedupingInterval: 5000,
|
|
42
|
+
revalidateOnFocus: false,
|
|
43
|
+
errorRetryCount: 1,
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const refresh = useCallback(() => {
|
|
48
|
+
mutate();
|
|
49
|
+
}, [mutate]);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
stats: data ?? null,
|
|
53
|
+
isLoading,
|
|
54
|
+
isError: !!error,
|
|
55
|
+
refresh,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import useSWR from "swr";
|
|
4
|
+
|
|
5
|
+
const SETTINGS_KEY = "/api/settings";
|
|
6
|
+
|
|
7
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
8
|
+
|
|
9
|
+
interface UserSettings {
|
|
10
|
+
displayName?: string;
|
|
11
|
+
avatarPath?: string;
|
|
12
|
+
tooltips?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shared hook for user settings (display name, avatar, etc).
|
|
17
|
+
*
|
|
18
|
+
* All consumers share the same SWR cache. Call `refresh()` after
|
|
19
|
+
* updating settings to instantly propagate changes everywhere
|
|
20
|
+
* (user menu, chat headers, message avatars).
|
|
21
|
+
*/
|
|
22
|
+
export function useUserSettings() {
|
|
23
|
+
const { data, mutate, isLoading } = useSWR<UserSettings>(
|
|
24
|
+
SETTINGS_KEY,
|
|
25
|
+
fetcher,
|
|
26
|
+
{
|
|
27
|
+
revalidateOnFocus: true,
|
|
28
|
+
dedupingInterval: 5000,
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const displayName = data?.displayName || "";
|
|
33
|
+
const avatarUrl = data?.avatarPath
|
|
34
|
+
? `/api/settings/avatar?v=${encodeURIComponent(data.avatarPath)}`
|
|
35
|
+
: null;
|
|
36
|
+
const tooltips = data?.tooltips !== "false"; // default true
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
displayName,
|
|
40
|
+
avatarUrl,
|
|
41
|
+
tooltips,
|
|
42
|
+
isLoading,
|
|
43
|
+
/** Call after saving settings to refresh all consumers */
|
|
44
|
+
refresh: () => mutate(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Channel
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export interface Channel {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
defaultAgentId: string;
|
|
9
|
+
agents: string[]; // Agent IDs in this channel
|
|
10
|
+
createdAt: number; // unix ms
|
|
11
|
+
archivedAt?: number | null; // unix ms — null if active
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Session
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface ChannelSession {
|
|
19
|
+
id: string;
|
|
20
|
+
channelId: string;
|
|
21
|
+
sessionKey: string | null; // Gateway session key
|
|
22
|
+
startedAt: number; // unix ms
|
|
23
|
+
endedAt: number | null; // unix ms
|
|
24
|
+
summary: string | null;
|
|
25
|
+
totalInputTokens: number;
|
|
26
|
+
totalOutputTokens: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Message
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export type MessageStatus = "complete" | "interrupted" | "aborted";
|
|
34
|
+
|
|
35
|
+
export interface ChatMessage {
|
|
36
|
+
id: string;
|
|
37
|
+
channelId: string;
|
|
38
|
+
sessionId: string | null;
|
|
39
|
+
senderType: "user" | "agent";
|
|
40
|
+
senderId: string;
|
|
41
|
+
senderName: string | null;
|
|
42
|
+
content: string;
|
|
43
|
+
status: MessageStatus;
|
|
44
|
+
mentionedAgentId: string | null;
|
|
45
|
+
runId: string | null; // Gateway run ID for streaming correlation
|
|
46
|
+
sessionKey: string | null; // Gateway session key
|
|
47
|
+
inputTokens: number | null;
|
|
48
|
+
outputTokens: number | null;
|
|
49
|
+
createdAt: number; // unix ms
|
|
50
|
+
attachments: MessageAttachment[];
|
|
51
|
+
reactions: MessageReaction[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Attachment
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
export interface MessageAttachment {
|
|
59
|
+
id: string;
|
|
60
|
+
messageId: string;
|
|
61
|
+
attachmentType: "image" | "audio";
|
|
62
|
+
filePath: string;
|
|
63
|
+
mimeType: string | null;
|
|
64
|
+
fileSize: number | null;
|
|
65
|
+
originalName: string | null;
|
|
66
|
+
createdAt: number; // unix ms
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Reaction
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
export interface MessageReaction {
|
|
74
|
+
id: string;
|
|
75
|
+
messageId: string;
|
|
76
|
+
agentId: string | null;
|
|
77
|
+
emoji: string;
|
|
78
|
+
emojiChar: string;
|
|
79
|
+
createdAt: number; // unix ms
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Streaming
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/** A message currently being streamed from the Gateway */
|
|
87
|
+
export interface StreamingMessage {
|
|
88
|
+
runId: string;
|
|
89
|
+
agentId: string;
|
|
90
|
+
agentName: string;
|
|
91
|
+
sessionKey: string;
|
|
92
|
+
content: string; // Accumulated text so far
|
|
93
|
+
startedAt: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A single streaming delta from the Gateway SSE */
|
|
97
|
+
export interface ChatDelta {
|
|
98
|
+
runId: string;
|
|
99
|
+
sessionKey: string;
|
|
100
|
+
seq: number;
|
|
101
|
+
state: "delta" | "final" | "error";
|
|
102
|
+
text?: string;
|
|
103
|
+
errorMessage?: string;
|
|
104
|
+
message?: {
|
|
105
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
106
|
+
role?: string;
|
|
107
|
+
inputTokens?: number;
|
|
108
|
+
outputTokens?: number;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Message Queue
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
export interface QueuedMessage {
|
|
117
|
+
id: string; // Temp ID for tracking
|
|
118
|
+
content: string;
|
|
119
|
+
agentId?: string;
|
|
120
|
+
attachments?: File[];
|
|
121
|
+
addedAt: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Session Status (from Gateway session.status RPC)
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
export interface SessionStatus {
|
|
129
|
+
sessionKey: string;
|
|
130
|
+
agentId: string;
|
|
131
|
+
model: string;
|
|
132
|
+
tokens: {
|
|
133
|
+
input: number;
|
|
134
|
+
output: number;
|
|
135
|
+
};
|
|
136
|
+
context: {
|
|
137
|
+
used: number;
|
|
138
|
+
limit: number;
|
|
139
|
+
percentage: number;
|
|
140
|
+
};
|
|
141
|
+
compactions: number;
|
|
142
|
+
runtime: string;
|
|
143
|
+
thinking: string;
|
|
144
|
+
updatedAt: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Storage Stats
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
export interface StorageStats {
|
|
152
|
+
messages: number;
|
|
153
|
+
channels: number;
|
|
154
|
+
attachments: number;
|
|
155
|
+
totalAttachmentBytes: number;
|
|
156
|
+
dbSizeBytes?: number; // Size of castle.db file
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// API Payloads
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
export interface ChatSendRequest {
|
|
164
|
+
channelId: string;
|
|
165
|
+
content: string;
|
|
166
|
+
agentId?: string;
|
|
167
|
+
attachmentIds?: string[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ChatSendResponse {
|
|
171
|
+
runId: string;
|
|
172
|
+
messageId: string;
|
|
173
|
+
sessionKey: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ChatCompleteRequest {
|
|
177
|
+
runId: string;
|
|
178
|
+
channelId: string;
|
|
179
|
+
content: string;
|
|
180
|
+
sessionKey: string;
|
|
181
|
+
agentId: string;
|
|
182
|
+
agentName?: string;
|
|
183
|
+
status: MessageStatus;
|
|
184
|
+
inputTokens?: number;
|
|
185
|
+
outputTokens?: number;
|
|
186
|
+
}
|