clawport-ui 0.8.2 → 0.8.5
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/README.md +21 -2
- package/app/agents/[id]/page.tsx +16 -0
- package/app/agents-provider.tsx +23 -0
- package/app/api/agents/fingerprint/route.ts +60 -0
- package/app/api/chat/[id]/route.ts +1 -1
- package/app/api/kanban/chat/[id]/route.ts +1 -1
- package/app/chat/page.tsx +2 -10
- package/app/crons/page.tsx +6 -12
- package/app/kanban/page.tsx +3 -15
- package/app/layout.tsx +3 -0
- package/app/page.tsx +23 -20
- package/app/settings/page.tsx +33 -15
- package/app/settings-provider.tsx +12 -1
- package/components/AgentNode.tsx +18 -0
- package/components/GlobalSearch.tsx +4 -13
- package/components/LiveStreamWidget.tsx +117 -12
- package/components/NavLinks.tsx +3 -18
- package/components/OnboardingWizard.test.tsx +168 -0
- package/components/OnboardingWizard.tsx +8 -3
- package/components/OrgMap.tsx +36 -16
- package/components/chat/ConversationView.tsx +1 -1
- package/components/costs/CostsPage.tsx +4 -8
- package/docs/OPENCLAW.md +121 -0
- package/docs/screenshots/activity.png +0 -0
- package/docs/screenshots/chat.png +0 -0
- package/docs/screenshots/costs.png +0 -0
- package/docs/screenshots/cron-schedule.png +0 -0
- package/docs/screenshots/kanban.png +0 -0
- package/docs/screenshots/live-logs.png +0 -0
- package/docs/screenshots/memory.png +0 -0
- package/docs/screenshots/org-map.png +0 -0
- package/docs/screenshots/pipelines.png +0 -0
- package/lib/agents-registry.test.ts +80 -0
- package/lib/agents-registry.ts +71 -9
- package/lib/agents.json +20 -0
- package/lib/agents.test.ts +56 -2
- package/lib/cli-utils.test.ts +44 -0
- package/lib/cli-utils.ts +25 -0
- package/lib/conversations.test.ts +1 -0
- package/lib/crons.test.ts +2 -1
- package/lib/crons.ts +19 -3
- package/lib/settings.test.ts +3 -0
- package/lib/settings.ts +9 -0
- package/lib/setup-detection.ts +37 -4
- package/lib/setup-scenarios.test.ts +7 -1
- package/lib/slash-commands.test.ts +1 -0
- package/lib/teams.test.ts +1 -0
- package/lib/types.ts +1 -0
- package/lib/useAgents.test.ts +241 -0
- package/lib/useAgents.ts +110 -0
- package/package.json +1 -1
- package/scripts/setup.mjs +24 -5
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/clawport-ui)
|
|
10
10
|
[](LICENSE)
|
|
11
|
-
[](#testing)
|
|
12
12
|
|
|
13
13
|
[Website](https://clawport.dev) | [Setup Guide](SETUP.md) | [API Docs](docs/API.md) | [npm](https://www.npmjs.com/package/clawport-ui)
|
|
14
14
|
|
|
@@ -20,6 +20,24 @@ ClawPort is an open-source dashboard for managing, monitoring, and talking direc
|
|
|
20
20
|
|
|
21
21
|
No separate AI API keys needed. Everything routes through your OpenClaw gateway.
|
|
22
22
|
|
|
23
|
+
<img src="docs/screenshots/org-map.png" alt="Org Map" width="100%" />
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary><strong>More screenshots</strong></summary>
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| <img src="docs/screenshots/chat.png" alt="Agent Chat" /> | <img src="docs/screenshots/kanban.png" alt="Kanban Board" /> |
|
|
31
|
+
| **Chat** -- streaming text, vision, voice, file attachments | **Kanban** -- drag-and-drop task board across agents |
|
|
32
|
+
| <img src="docs/screenshots/pipelines.png" alt="Cron Pipelines" /> | <img src="docs/screenshots/cron-schedule.png" alt="Cron Schedule" /> |
|
|
33
|
+
| **Pipelines** -- DAG visualization with health checks | **Schedule** -- weekly heatmap and job management |
|
|
34
|
+
| <img src="docs/screenshots/activity.png" alt="Activity Console" /> | <img src="docs/screenshots/live-logs.png" alt="Live Logs" /> |
|
|
35
|
+
| **Activity** -- historical log browser with JSON expansion | **Live Logs** -- real-time streaming widget |
|
|
36
|
+
| <img src="docs/screenshots/costs.png" alt="Cost Dashboard" /> | <img src="docs/screenshots/memory.png" alt="Memory Browser" /> |
|
|
37
|
+
| **Costs** -- token usage, anomalies, optimization insights | **Memory** -- team memory browser with markdown rendering |
|
|
38
|
+
|
|
39
|
+
</details>
|
|
40
|
+
|
|
23
41
|
---
|
|
24
42
|
|
|
25
43
|
## Quick Start
|
|
@@ -171,7 +189,7 @@ clawport help # Show usage
|
|
|
171
189
|
## Testing
|
|
172
190
|
|
|
173
191
|
```bash
|
|
174
|
-
npm test #
|
|
192
|
+
npm test # 781 tests across 32 suites (Vitest)
|
|
175
193
|
npx tsc --noEmit # Type-check (zero errors)
|
|
176
194
|
npx next build # Production build
|
|
177
195
|
```
|
|
@@ -199,6 +217,7 @@ npx next build # Production build
|
|
|
199
217
|
| [docs/THEMING.md](docs/THEMING.md) | Theme system, CSS tokens, settings API |
|
|
200
218
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to contribute |
|
|
201
219
|
| [CHANGELOG.md](CHANGELOG.md) | Version history |
|
|
220
|
+
| [docs/OPENCLAW.md](docs/OPENCLAW.md) | OpenClaw integration reference |
|
|
202
221
|
| [CLAUDE.md](CLAUDE.md) | Developer architecture guide |
|
|
203
222
|
|
|
204
223
|
---
|
package/app/agents/[id]/page.tsx
CHANGED
|
@@ -490,6 +490,22 @@ export default function AgentDetailPage({
|
|
|
490
490
|
>
|
|
491
491
|
{agent.title}
|
|
492
492
|
</p>
|
|
493
|
+
{agent.model && (
|
|
494
|
+
<span
|
|
495
|
+
style={{
|
|
496
|
+
display: "inline-block",
|
|
497
|
+
marginTop: "var(--space-1)",
|
|
498
|
+
fontSize: "var(--text-caption2)",
|
|
499
|
+
fontFamily: "var(--font-mono)",
|
|
500
|
+
color: "var(--text-tertiary)",
|
|
501
|
+
background: "var(--fill-secondary)",
|
|
502
|
+
padding: "1px 8px",
|
|
503
|
+
borderRadius: 6,
|
|
504
|
+
}}
|
|
505
|
+
>
|
|
506
|
+
{agent.model.split("/").pop()}
|
|
507
|
+
</span>
|
|
508
|
+
)}
|
|
493
509
|
{/* Color swatch */}
|
|
494
510
|
<div
|
|
495
511
|
style={{
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react'
|
|
4
|
+
import { useAgents, type UseAgentsResult } from '@/lib/useAgents'
|
|
5
|
+
|
|
6
|
+
const AgentsContext = createContext<UseAgentsResult>({
|
|
7
|
+
agents: [],
|
|
8
|
+
loading: true,
|
|
9
|
+
error: null,
|
|
10
|
+
refresh: () => {},
|
|
11
|
+
lastUpdated: null,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export function AgentsProvider({ children }: { children: React.ReactNode }) {
|
|
15
|
+
const agentsState = useAgents()
|
|
16
|
+
return (
|
|
17
|
+
<AgentsContext.Provider value={agentsState}>
|
|
18
|
+
{children}
|
|
19
|
+
</AgentsContext.Provider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useAgentsContext = () => useContext(AgentsContext)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { statSync, readdirSync, existsSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lightweight fingerprint endpoint for agent change detection.
|
|
7
|
+
*
|
|
8
|
+
* Returns a cheap hash based on:
|
|
9
|
+
* - agents/ directory mtime + entry count
|
|
10
|
+
* - clawport/agents.json mtime (if exists)
|
|
11
|
+
* - root SOUL.md mtime (if exists)
|
|
12
|
+
*
|
|
13
|
+
* Avoids the expensive parts of loadRegistry() (reading file content,
|
|
14
|
+
* execSync CLI calls, multi-workspace merging).
|
|
15
|
+
*/
|
|
16
|
+
export async function GET() {
|
|
17
|
+
const workspacePath = process.env.WORKSPACE_PATH
|
|
18
|
+
|
|
19
|
+
if (!workspacePath) {
|
|
20
|
+
return NextResponse.json({ fingerprint: 'no-workspace' })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parts: (string | number)[] = []
|
|
24
|
+
|
|
25
|
+
// agents/ directory: mtime + entry count
|
|
26
|
+
const agentsDir = join(workspacePath, 'agents')
|
|
27
|
+
if (existsSync(agentsDir)) {
|
|
28
|
+
try {
|
|
29
|
+
const stat = statSync(agentsDir)
|
|
30
|
+
const entries = readdirSync(agentsDir)
|
|
31
|
+
parts.push(stat.mtimeMs, entries.length)
|
|
32
|
+
} catch {
|
|
33
|
+
parts.push('agents-err')
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
parts.push('no-agents-dir')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// clawport/agents.json user override mtime
|
|
40
|
+
const userRegistry = join(workspacePath, 'clawport', 'agents.json')
|
|
41
|
+
if (existsSync(userRegistry)) {
|
|
42
|
+
try {
|
|
43
|
+
parts.push(statSync(userRegistry).mtimeMs)
|
|
44
|
+
} catch {
|
|
45
|
+
parts.push('override-err')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Root SOUL.md mtime
|
|
50
|
+
const rootSoul = join(workspacePath, 'SOUL.md')
|
|
51
|
+
if (existsSync(rootSoul)) {
|
|
52
|
+
try {
|
|
53
|
+
parts.push(statSync(rootSoul).mtimeMs)
|
|
54
|
+
} catch {
|
|
55
|
+
parts.push('soul-err')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return NextResponse.json({ fingerprint: JSON.stringify(parts) })
|
|
60
|
+
}
|
|
@@ -93,7 +93,7 @@ export async function POST(
|
|
|
93
93
|
|
|
94
94
|
try {
|
|
95
95
|
const stream = await openai.chat.completions.create({
|
|
96
|
-
model: 'claude-sonnet-4-6',
|
|
96
|
+
model: agent.model || 'claude-sonnet-4-6',
|
|
97
97
|
stream: true,
|
|
98
98
|
messages: [
|
|
99
99
|
{ role: 'system' as const, content: systemPrompt },
|
|
@@ -90,7 +90,7 @@ Help the user with this ticket. Stay in character as ${agent.name}, ${agent.titl
|
|
|
90
90
|
|
|
91
91
|
try {
|
|
92
92
|
const stream = await openai.chat.completions.create({
|
|
93
|
-
model: 'claude-sonnet-4-6',
|
|
93
|
+
model: agent.model || 'claude-sonnet-4-6',
|
|
94
94
|
stream: true,
|
|
95
95
|
messages: [
|
|
96
96
|
{ role: 'system' as const, content: systemPrompt },
|
package/app/chat/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
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
|
+
import { useAgentsContext } from '@/app/agents-provider'
|
|
5
6
|
import { AgentList, AgentListMobile } from '@/components/chat/AgentList'
|
|
6
7
|
import { ConversationView } from '@/components/chat/ConversationView'
|
|
7
8
|
import {
|
|
@@ -13,20 +14,11 @@ import {
|
|
|
13
14
|
function MessengerApp() {
|
|
14
15
|
const router = useRouter()
|
|
15
16
|
const searchParams = useSearchParams()
|
|
16
|
-
const
|
|
17
|
+
const { agents, loading } = useAgentsContext()
|
|
17
18
|
const [conversations, setConversations] = useState<ConversationStore>({})
|
|
18
19
|
const [activeAgentId, setActiveAgentId] = useState<string | null>(searchParams.get('agent'))
|
|
19
|
-
const [loading, setLoading] = useState(true)
|
|
20
20
|
const [mobileShowConversation, setMobileShowConversation] = useState(!!searchParams.get('agent'))
|
|
21
21
|
|
|
22
|
-
// Load agents
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
fetch('/api/agents').then(r => r.json()).then((data: Agent[]) => {
|
|
25
|
-
setAgents(data)
|
|
26
|
-
setLoading(false)
|
|
27
|
-
})
|
|
28
|
-
}, [])
|
|
29
|
-
|
|
30
22
|
// Load conversations from localStorage
|
|
31
23
|
useEffect(() => {
|
|
32
24
|
setConversations(loadConversations())
|
package/app/crons/page.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import type { Agent, CronJob, CronRun } from "@/lib/types";
|
|
6
|
+
import { useAgentsContext } from "@/app/agents-provider";
|
|
6
7
|
import type { Pipeline } from "@/lib/cron-pipelines";
|
|
7
8
|
import { formatDuration, timeAgo, nextRunLabel } from "@/lib/cron-utils";
|
|
8
9
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
@@ -390,8 +391,8 @@ function RecentRuns({ jobId }: { jobId: string }) {
|
|
|
390
391
|
/* ─── Component ─────────────────────────────────────────────────── */
|
|
391
392
|
|
|
392
393
|
export default function CronsPage() {
|
|
394
|
+
const { agents } = useAgentsContext();
|
|
393
395
|
const [crons, setCrons] = useState<CronJob[]>([]);
|
|
394
|
-
const [agents, setAgents] = useState<Agent[]>([]);
|
|
395
396
|
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
|
|
396
397
|
const [filter, setFilter] = useState<Filter>("all");
|
|
397
398
|
const [tab, setTab] = useState<Tab>("overview");
|
|
@@ -410,18 +411,12 @@ export default function CronsPage() {
|
|
|
410
411
|
const refresh = useCallback(() => {
|
|
411
412
|
setRefreshing(true);
|
|
412
413
|
setError(null);
|
|
413
|
-
|
|
414
|
-
|
|
414
|
+
fetch("/api/crons")
|
|
415
|
+
.then((r) => {
|
|
415
416
|
if (!r.ok) throw new Error("Failed to load crons");
|
|
416
417
|
return r.json();
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
if (!r.ok) throw new Error("Failed to load agents");
|
|
420
|
-
return r.json();
|
|
421
|
-
}),
|
|
422
|
-
])
|
|
423
|
-
.then(([cronData, a]) => {
|
|
424
|
-
// Backward compat: if response is a plain array, treat as crons-only
|
|
418
|
+
})
|
|
419
|
+
.then((cronData) => {
|
|
425
420
|
if (Array.isArray(cronData)) {
|
|
426
421
|
setCrons(cronData);
|
|
427
422
|
setPipelines([]);
|
|
@@ -429,7 +424,6 @@ export default function CronsPage() {
|
|
|
429
424
|
setCrons(cronData.crons);
|
|
430
425
|
setPipelines(cronData.pipelines || []);
|
|
431
426
|
}
|
|
432
|
-
setAgents(a);
|
|
433
427
|
setLastRefresh(new Date());
|
|
434
428
|
setLoading(false);
|
|
435
429
|
setRefreshing(false);
|
package/app/kanban/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import type { Agent } from '@/lib/types'
|
|
5
|
+
import { useAgentsContext } from '@/app/agents-provider'
|
|
5
6
|
import type { KanbanTicket, TicketStatus, TicketPriority, TeamRole } from '@/lib/kanban/types'
|
|
6
7
|
import {
|
|
7
8
|
loadTickets,
|
|
@@ -23,30 +24,17 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|
|
23
24
|
|
|
24
25
|
export default function KanbanPage() {
|
|
25
26
|
const [tickets, setTickets] = useState<KanbanStore>({})
|
|
26
|
-
const
|
|
27
|
+
const { agents, loading: agentsLoading, error, refresh: refreshAgents } = useAgentsContext()
|
|
27
28
|
const [loading, setLoading] = useState(true)
|
|
28
|
-
const [error, setError] = useState<string | null>(null)
|
|
29
29
|
const [createOpen, setCreateOpen] = useState(false)
|
|
30
30
|
const [selectedTicket, setSelectedTicket] = useState<KanbanTicket | null>(null)
|
|
31
31
|
const [filterAgentId, setFilterAgentId] = useState<string | null>(null)
|
|
32
32
|
|
|
33
33
|
const loadData = useCallback(() => {
|
|
34
|
-
setLoading(true)
|
|
35
|
-
setError(null)
|
|
36
|
-
|
|
37
34
|
// Load tickets from localStorage
|
|
38
35
|
const stored = loadTickets()
|
|
39
36
|
setTickets(stored)
|
|
40
|
-
|
|
41
|
-
// Load agents from API
|
|
42
|
-
fetch('/api/agents')
|
|
43
|
-
.then((r) => {
|
|
44
|
-
if (!r.ok) throw new Error('Failed to fetch agents')
|
|
45
|
-
return r.json()
|
|
46
|
-
})
|
|
47
|
-
.then((a: Agent[]) => setAgents(a))
|
|
48
|
-
.catch((e) => setError(e.message))
|
|
49
|
-
.finally(() => setLoading(false))
|
|
37
|
+
setLoading(false)
|
|
50
38
|
}, [])
|
|
51
39
|
|
|
52
40
|
useEffect(() => {
|
package/app/layout.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { Metadata } from 'next';
|
|
|
2
2
|
import './globals.css';
|
|
3
3
|
import { ThemeProvider } from './providers';
|
|
4
4
|
import { SettingsProvider } from './settings-provider';
|
|
5
|
+
import { AgentsProvider } from './agents-provider';
|
|
5
6
|
import { Sidebar } from '@/components/Sidebar';
|
|
6
7
|
import { DynamicFavicon } from '@/components/DynamicFavicon';
|
|
7
8
|
import { OnboardingWizard } from '@/components/OnboardingWizard';
|
|
@@ -22,6 +23,7 @@ export default function RootLayout({
|
|
|
22
23
|
<body>
|
|
23
24
|
<ThemeProvider>
|
|
24
25
|
<SettingsProvider>
|
|
26
|
+
<AgentsProvider>
|
|
25
27
|
<DynamicFavicon />
|
|
26
28
|
<OnboardingWizard />
|
|
27
29
|
<LiveStreamWidget />
|
|
@@ -39,6 +41,7 @@ export default function RootLayout({
|
|
|
39
41
|
{children}
|
|
40
42
|
</main>
|
|
41
43
|
</div>
|
|
44
|
+
</AgentsProvider>
|
|
42
45
|
</SettingsProvider>
|
|
43
46
|
</ThemeProvider>
|
|
44
47
|
</body>
|
package/app/page.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useRouter } from "next/navigation"
|
|
|
4
4
|
import Link from "next/link"
|
|
5
5
|
import dynamic from "next/dynamic"
|
|
6
6
|
import type { Agent, CronJob } from "@/lib/types"
|
|
7
|
+
import { useAgentsContext } from "@/app/agents-provider"
|
|
7
8
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
8
9
|
import { Map as MapIcon, LayoutGrid, List, X, MessageSquare, User } from "lucide-react"
|
|
9
10
|
import { ErrorState } from "@/components/ErrorState"
|
|
@@ -117,38 +118,40 @@ const VIEW_OPTIONS: { key: View; label: string }[] = [
|
|
|
117
118
|
────────────────────────────────────────────── */
|
|
118
119
|
export default function HomePage() {
|
|
119
120
|
const router = useRouter()
|
|
120
|
-
const
|
|
121
|
+
const { agents, loading: agentsLoading, error: agentsError, refresh: refreshAgents } = useAgentsContext()
|
|
121
122
|
const [crons, setCrons] = useState<CronJob[]>([])
|
|
122
123
|
const [selected, setSelected] = useState<Agent | null>(null)
|
|
123
|
-
const [
|
|
124
|
-
const [
|
|
124
|
+
const [cronsLoading, setCronsLoading] = useState(true)
|
|
125
|
+
const [cronsError, setCronsError] = useState<string | null>(null)
|
|
125
126
|
const [view, setView] = useState<View>("map")
|
|
126
127
|
const closeRef = useRef<HTMLButtonElement>(null)
|
|
127
128
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
fetch("/api/crons").then((r) => {
|
|
129
|
+
const loading = agentsLoading || cronsLoading
|
|
130
|
+
const error = agentsError || cronsError
|
|
131
|
+
|
|
132
|
+
const loadCrons = useCallback(() => {
|
|
133
|
+
setCronsLoading(true)
|
|
134
|
+
setCronsError(null)
|
|
135
|
+
fetch("/api/crons")
|
|
136
|
+
.then((r) => {
|
|
137
137
|
if (!r.ok) throw new Error("Failed to fetch crons")
|
|
138
138
|
return r.json()
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
.then(([a, cronData]) => {
|
|
142
|
-
setAgents(a)
|
|
139
|
+
})
|
|
140
|
+
.then((cronData) => {
|
|
143
141
|
setCrons(Array.isArray(cronData) ? cronData : cronData.crons ?? [])
|
|
144
142
|
})
|
|
145
|
-
.catch((e) =>
|
|
146
|
-
.finally(() =>
|
|
143
|
+
.catch((e) => setCronsError(e.message))
|
|
144
|
+
.finally(() => setCronsLoading(false))
|
|
147
145
|
}, [])
|
|
148
146
|
|
|
149
147
|
useEffect(() => {
|
|
150
|
-
|
|
151
|
-
}, [
|
|
148
|
+
loadCrons()
|
|
149
|
+
}, [loadCrons])
|
|
150
|
+
|
|
151
|
+
const loadData = useCallback(() => {
|
|
152
|
+
refreshAgents()
|
|
153
|
+
loadCrons()
|
|
154
|
+
}, [refreshAgents, loadCrons])
|
|
152
155
|
|
|
153
156
|
// Focus close button when panel opens
|
|
154
157
|
useEffect(() => {
|
package/app/settings/page.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react'
|
|
4
|
-
import { ChevronRight, RotateCcw, Trash2, Upload, X } from 'lucide-react'
|
|
4
|
+
import { ChevronRight, RotateCcw, Trash2, Upload, X, RefreshCw } from 'lucide-react'
|
|
5
5
|
import type { Agent } from '@/lib/types'
|
|
6
6
|
import { useSettings } from '@/app/settings-provider'
|
|
7
|
+
import { useAgentsContext } from '@/app/agents-provider'
|
|
7
8
|
import { AgentAvatar } from '@/components/AgentAvatar'
|
|
8
9
|
import { OnboardingWizard } from '@/components/OnboardingWizard'
|
|
9
10
|
import { deleteOnServer } from '@/lib/conversations'
|
|
@@ -75,9 +76,10 @@ export default function SettingsPage() {
|
|
|
75
76
|
resetAll,
|
|
76
77
|
} = useSettings()
|
|
77
78
|
|
|
79
|
+
const { agents, refresh: refreshAgents, loading: agentsLoading } = useAgentsContext()
|
|
78
80
|
const [wizardOpen, setWizardOpen] = useState(false)
|
|
79
|
-
const [agents, setAgents] = useState<Agent[]>([])
|
|
80
81
|
const [expandedAgent, setExpandedAgent] = useState<string | null>(null)
|
|
82
|
+
const [rescanResult, setRescanResult] = useState<string | null>(null)
|
|
81
83
|
const [nameValue, setNameValue] = useState(settings.portalName ?? '')
|
|
82
84
|
const [subtitleValue, setSubtitleValue] = useState(settings.portalSubtitle ?? '')
|
|
83
85
|
const [operatorNameValue, setOperatorNameValue] = useState(settings.operatorName ?? '')
|
|
@@ -93,19 +95,6 @@ export default function SettingsPage() {
|
|
|
93
95
|
setEmojiValue(settings.portalEmoji ?? '')
|
|
94
96
|
}, [settings.portalName, settings.portalSubtitle, settings.operatorName, settings.portalEmoji])
|
|
95
97
|
|
|
96
|
-
// Fetch agents
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
fetch('/api/agents')
|
|
99
|
-
.then((r) => {
|
|
100
|
-
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
101
|
-
return r.json()
|
|
102
|
-
})
|
|
103
|
-
.then((data: unknown) => {
|
|
104
|
-
if (Array.isArray(data)) setAgents(data as Agent[])
|
|
105
|
-
})
|
|
106
|
-
.catch(() => setAgents([]))
|
|
107
|
-
}, [])
|
|
108
|
-
|
|
109
98
|
async function handleIconUpload(file: File) {
|
|
110
99
|
try {
|
|
111
100
|
const dataUrl = await resizeImage(file, 200)
|
|
@@ -897,6 +886,35 @@ export default function SettingsPage() {
|
|
|
897
886
|
<RotateCcw size={16} />
|
|
898
887
|
Re-run Setup
|
|
899
888
|
</button>
|
|
889
|
+
<button
|
|
890
|
+
onClick={() => {
|
|
891
|
+
refreshAgents()
|
|
892
|
+
setRescanResult(null)
|
|
893
|
+
// Show result after a short delay to let the fetch complete
|
|
894
|
+
setTimeout(() => {
|
|
895
|
+
setRescanResult(`Found ${agents.length} agents`)
|
|
896
|
+
setTimeout(() => setRescanResult(null), 2000)
|
|
897
|
+
}, 600)
|
|
898
|
+
}}
|
|
899
|
+
className="btn-scale"
|
|
900
|
+
style={{
|
|
901
|
+
padding: 'var(--space-2) var(--space-6)',
|
|
902
|
+
borderRadius: 'var(--radius-md)',
|
|
903
|
+
background: 'var(--fill-tertiary)',
|
|
904
|
+
color: 'var(--text-primary)',
|
|
905
|
+
border: 'none',
|
|
906
|
+
cursor: 'pointer',
|
|
907
|
+
fontSize: 'var(--text-body)',
|
|
908
|
+
fontWeight: 'var(--weight-semibold)',
|
|
909
|
+
transition: 'all 150ms var(--ease-spring)',
|
|
910
|
+
display: 'inline-flex',
|
|
911
|
+
alignItems: 'center',
|
|
912
|
+
gap: 'var(--space-2)',
|
|
913
|
+
}}
|
|
914
|
+
>
|
|
915
|
+
<RefreshCw size={16} className={agentsLoading ? 'animate-spin' : ''} />
|
|
916
|
+
{rescanResult || 'Rescan Agents'}
|
|
917
|
+
</button>
|
|
900
918
|
<button
|
|
901
919
|
onClick={() => {
|
|
902
920
|
if (window.confirm('Reset all settings to defaults?')) {
|
|
@@ -31,11 +31,12 @@ interface SettingsContextValue {
|
|
|
31
31
|
setAgentOverride: (agentId: string, override: AgentOverride) => void
|
|
32
32
|
clearAgentOverride: (agentId: string) => void
|
|
33
33
|
getAgentDisplay: (agent: Agent) => AgentDisplay
|
|
34
|
+
setLiveStreamPosition: (pos: { x: number; y: number } | null) => void
|
|
34
35
|
resetAll: () => void
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const SettingsContext = createContext<SettingsContextValue>({
|
|
38
|
-
settings: { accentColor: null, portalName: null, portalSubtitle: null, portalEmoji: null, portalIcon: null, iconBgHidden: false, emojiOnly: false, operatorName: null, agentOverrides: {} },
|
|
39
|
+
settings: { accentColor: null, portalName: null, portalSubtitle: null, portalEmoji: null, portalIcon: null, iconBgHidden: false, emojiOnly: false, operatorName: null, agentOverrides: {}, liveStreamPosition: null },
|
|
39
40
|
setAccentColor: () => {},
|
|
40
41
|
setPortalName: () => {},
|
|
41
42
|
setPortalSubtitle: () => {},
|
|
@@ -47,6 +48,7 @@ const SettingsContext = createContext<SettingsContextValue>({
|
|
|
47
48
|
setAgentOverride: () => {},
|
|
48
49
|
clearAgentOverride: () => {},
|
|
49
50
|
getAgentDisplay: (agent) => ({ emoji: agent.emoji }),
|
|
51
|
+
setLiveStreamPosition: () => {},
|
|
50
52
|
resetAll: () => {},
|
|
51
53
|
})
|
|
52
54
|
|
|
@@ -156,6 +158,13 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) {
|
|
|
156
158
|
[settings, update],
|
|
157
159
|
)
|
|
158
160
|
|
|
161
|
+
const setLiveStreamPosition = useCallback(
|
|
162
|
+
(pos: { x: number; y: number } | null) => {
|
|
163
|
+
update({ ...settings, liveStreamPosition: pos })
|
|
164
|
+
},
|
|
165
|
+
[settings, update],
|
|
166
|
+
)
|
|
167
|
+
|
|
159
168
|
const getAgentDisplay = useCallback(
|
|
160
169
|
(agent: Agent): AgentDisplay => {
|
|
161
170
|
const override = settings.agentOverrides[agent.id]
|
|
@@ -179,6 +188,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) {
|
|
|
179
188
|
emojiOnly: false,
|
|
180
189
|
operatorName: null,
|
|
181
190
|
agentOverrides: {},
|
|
191
|
+
liveStreamPosition: null,
|
|
182
192
|
}
|
|
183
193
|
update(defaults)
|
|
184
194
|
}, [update])
|
|
@@ -198,6 +208,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) {
|
|
|
198
208
|
setAgentOverride,
|
|
199
209
|
clearAgentOverride,
|
|
200
210
|
getAgentDisplay,
|
|
211
|
+
setLiveStreamPosition,
|
|
201
212
|
resetAll,
|
|
202
213
|
}}
|
|
203
214
|
>
|
package/components/AgentNode.tsx
CHANGED
|
@@ -129,6 +129,24 @@ export function AgentNode({ data, selected }: NodeProps) {
|
|
|
129
129
|
{reportCount} reports
|
|
130
130
|
</span>
|
|
131
131
|
)}
|
|
132
|
+
{agent.model && (
|
|
133
|
+
<span
|
|
134
|
+
style={{
|
|
135
|
+
fontSize: "var(--text-caption2)",
|
|
136
|
+
fontWeight: "var(--weight-medium)",
|
|
137
|
+
color: "var(--text-tertiary)",
|
|
138
|
+
background: "var(--fill-tertiary)",
|
|
139
|
+
padding: "1px 7px",
|
|
140
|
+
borderRadius: 10,
|
|
141
|
+
overflow: "hidden",
|
|
142
|
+
textOverflow: "ellipsis",
|
|
143
|
+
whiteSpace: "nowrap",
|
|
144
|
+
maxWidth: 120,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{agent.model.split("/").pop()}
|
|
148
|
+
</span>
|
|
149
|
+
)}
|
|
132
150
|
{hasCrons && (
|
|
133
151
|
<span
|
|
134
152
|
style={{
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
Timer,
|
|
13
13
|
Settings,
|
|
14
14
|
} from 'lucide-react';
|
|
15
|
-
import type {
|
|
15
|
+
import type { CronJob } from '@/lib/types';
|
|
16
|
+
import { useAgentsContext } from '@/app/agents-provider';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Types
|
|
@@ -110,7 +111,7 @@ export function GlobalSearch() {
|
|
|
110
111
|
const [open, setOpen] = useState(false);
|
|
111
112
|
const [query, setQuery] = useState('');
|
|
112
113
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
113
|
-
const
|
|
114
|
+
const { agents } = useAgentsContext();
|
|
114
115
|
const [crons, setCrons] = useState<CronJob[]>([]);
|
|
115
116
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
116
117
|
const listRef = useRef<HTMLDivElement>(null);
|
|
@@ -149,17 +150,7 @@ export function GlobalSearch() {
|
|
|
149
150
|
// Reset state
|
|
150
151
|
setQuery('');
|
|
151
152
|
setActiveIndex(0);
|
|
152
|
-
// Fetch agents
|
|
153
|
-
fetch('/api/agents')
|
|
154
|
-
.then((r) => {
|
|
155
|
-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
156
|
-
return r.json();
|
|
157
|
-
})
|
|
158
|
-
.then((data: unknown) => {
|
|
159
|
-
if (Array.isArray(data)) setAgents(data as Agent[]);
|
|
160
|
-
})
|
|
161
|
-
.catch(() => setAgents([]));
|
|
162
|
-
// Fetch crons
|
|
153
|
+
// Fetch crons (agents come from context)
|
|
163
154
|
fetch('/api/crons')
|
|
164
155
|
.then((r) => {
|
|
165
156
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|