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.
Files changed (52) hide show
  1. package/README.md +21 -2
  2. package/app/agents/[id]/page.tsx +16 -0
  3. package/app/agents-provider.tsx +23 -0
  4. package/app/api/agents/fingerprint/route.ts +60 -0
  5. package/app/api/chat/[id]/route.ts +1 -1
  6. package/app/api/kanban/chat/[id]/route.ts +1 -1
  7. package/app/chat/page.tsx +2 -10
  8. package/app/crons/page.tsx +6 -12
  9. package/app/kanban/page.tsx +3 -15
  10. package/app/layout.tsx +3 -0
  11. package/app/page.tsx +23 -20
  12. package/app/settings/page.tsx +33 -15
  13. package/app/settings-provider.tsx +12 -1
  14. package/components/AgentNode.tsx +18 -0
  15. package/components/GlobalSearch.tsx +4 -13
  16. package/components/LiveStreamWidget.tsx +117 -12
  17. package/components/NavLinks.tsx +3 -18
  18. package/components/OnboardingWizard.test.tsx +168 -0
  19. package/components/OnboardingWizard.tsx +8 -3
  20. package/components/OrgMap.tsx +36 -16
  21. package/components/chat/ConversationView.tsx +1 -1
  22. package/components/costs/CostsPage.tsx +4 -8
  23. package/docs/OPENCLAW.md +121 -0
  24. package/docs/screenshots/activity.png +0 -0
  25. package/docs/screenshots/chat.png +0 -0
  26. package/docs/screenshots/costs.png +0 -0
  27. package/docs/screenshots/cron-schedule.png +0 -0
  28. package/docs/screenshots/kanban.png +0 -0
  29. package/docs/screenshots/live-logs.png +0 -0
  30. package/docs/screenshots/memory.png +0 -0
  31. package/docs/screenshots/org-map.png +0 -0
  32. package/docs/screenshots/pipelines.png +0 -0
  33. package/lib/agents-registry.test.ts +80 -0
  34. package/lib/agents-registry.ts +71 -9
  35. package/lib/agents.json +20 -0
  36. package/lib/agents.test.ts +56 -2
  37. package/lib/cli-utils.test.ts +44 -0
  38. package/lib/cli-utils.ts +25 -0
  39. package/lib/conversations.test.ts +1 -0
  40. package/lib/crons.test.ts +2 -1
  41. package/lib/crons.ts +19 -3
  42. package/lib/settings.test.ts +3 -0
  43. package/lib/settings.ts +9 -0
  44. package/lib/setup-detection.ts +37 -4
  45. package/lib/setup-scenarios.test.ts +7 -1
  46. package/lib/slash-commands.test.ts +1 -0
  47. package/lib/teams.test.ts +1 -0
  48. package/lib/types.ts +1 -0
  49. package/lib/useAgents.test.ts +241 -0
  50. package/lib/useAgents.ts +110 -0
  51. package/package.json +1 -1
  52. package/scripts/setup.mjs +24 -5
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/clawport-ui.svg)](https://www.npmjs.com/package/clawport-ui)
10
10
  [![license](https://img.shields.io/npm/l/clawport-ui.svg)](LICENSE)
11
- [![tests](https://img.shields.io/badge/tests-536%20passed-brightgreen)](#testing)
11
+ [![tests](https://img.shields.io/badge/tests-781%20passed-brightgreen)](#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 # 536 tests across 24 suites (Vitest)
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
  ---
@@ -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 [agents, setAgents] = useState<Agent[]>([])
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())
@@ -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
- Promise.all([
414
- fetch("/api/crons").then((r) => {
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
- fetch("/api/agents").then((r) => {
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);
@@ -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 [agents, setAgents] = useState<Agent[]>([])
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 [agents, setAgents] = useState<Agent[]>([])
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 [loading, setLoading] = useState(true)
124
- const [error, setError] = useState<string | null>(null)
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 loadData = useCallback(() => {
129
- setLoading(true)
130
- setError(null)
131
- Promise.all([
132
- fetch("/api/agents").then((r) => {
133
- if (!r.ok) throw new Error("Failed to fetch agents")
134
- return r.json()
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) => setError(e.message))
146
- .finally(() => setLoading(false))
143
+ .catch((e) => setCronsError(e.message))
144
+ .finally(() => setCronsLoading(false))
147
145
  }, [])
148
146
 
149
147
  useEffect(() => {
150
- loadData()
151
- }, [loadData])
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(() => {
@@ -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
  >
@@ -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 { Agent, CronJob } from '@/lib/types';
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 [agents, setAgents] = useState<Agent[]>([]);
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}`);