Skip to content

Commit 328ce79

Browse files
authored
Merge pull request #23 from AgentWorkforce/updated-models
Updated models & display improvements
2 parents 7e5066e + 7478880 commit 328ce79

7 files changed

Lines changed: 245 additions & 120 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ node_modules/
77

88
# Build outputs
99
dist/
10+
dist
1011
out/
12+
out
1113
.next/
1214
*.tsbuildinfo
1315

packages/dashboard-server/src/server.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ interface AgentStatus {
390390
isSpawned?: boolean;
391391
team?: string;
392392
avatarUrl?: string;
393+
model?: string;
393394
}
394395

395396
interface Attachment {
@@ -997,25 +998,56 @@ export async function startDashboard(
997998
app.use(express.static(dashboardDir, { extensions: ['html'] }));
998999

9991000
// Fallback for Next.js pages (e.g., /metrics -> /metrics.html)
1000-
// These are needed when a route exists as both a directory and .html file
1001-
const sendFileWithFallback = (res: express.Response, filePath: string) => {
1001+
// These are needed when a route exists as both a directory and .html file.
1002+
// For /app/* deep links we prefer redirecting to "/" if the export is missing,
1003+
// so users don’t get stuck on a plain-text error on refresh.
1004+
const uiMissingMessage =
1005+
'Dashboard UI file not found. Please reinstall using: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash';
1006+
1007+
const sendFileOr = (
1008+
res: express.Response,
1009+
filePath: string,
1010+
onError: (err: Error) => void
1011+
) => {
10021012
res.sendFile(filePath, (err) => {
10031013
if (err && !res.headersSent) {
1004-
res.status(404).send('Dashboard UI file not found. Please reinstall using: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash');
1014+
onError(err);
10051015
}
10061016
});
10071017
};
10081018

1019+
const sendFileOrText404 = (res: express.Response, filePath: string, message: string) => {
1020+
sendFileOr(res, filePath, () => {
1021+
res.status(404).send(message);
1022+
});
1023+
};
1024+
1025+
const sendFileOrRedirectRoot = (res: express.Response, filePath: string) => {
1026+
sendFileOr(res, filePath, () => {
1027+
// If the app entrypoint isn’t present, try to recover by sending users
1028+
// to the root page (if it exists). Otherwise keep the install hint.
1029+
if (fs.existsSync(path.join(dashboardDir, 'index.html'))) {
1030+
res.redirect(302, '/');
1031+
return;
1032+
}
1033+
res.status(404).send(uiMissingMessage);
1034+
});
1035+
};
1036+
10091037
app.get('/metrics', (req, res) => {
1010-
sendFileWithFallback(res, path.join(dashboardDir, 'metrics.html'));
1038+
sendFileOrText404(
1039+
res,
1040+
path.join(dashboardDir, 'metrics.html'),
1041+
uiMissingMessage
1042+
);
10111043
});
10121044
app.get('/app', (req, res) => {
1013-
sendFileWithFallback(res, path.join(dashboardDir, 'app.html'));
1045+
sendFileOrRedirectRoot(res, path.join(dashboardDir, 'app.html'));
10141046
});
10151047
// Catch-all for /app/* routes - serve app.html and let client-side routing handle it
10161048
// Express 5 requires named parameter for wildcards
10171049
app.get('/app/{*path}', (req, res) => {
1018-
sendFileWithFallback(res, path.join(dashboardDir, 'app.html'));
1050+
sendFileOrRedirectRoot(res, path.join(dashboardDir, 'app.html'));
10191051
});
10201052
} else {
10211053
// Serve a fallback page when dashboard UI files aren't available
@@ -2097,7 +2129,7 @@ export async function startDashboard(
20972129
}
20982130
}
20992131

2100-
// Mark spawned agents with isSpawned flag and team
2132+
// Mark spawned agents with isSpawned flag, team, and model
21012133
if (spawnReader) {
21022134
const activeWorkers = spawnReader.getActiveWorkers();
21032135
for (const worker of activeWorkers) {
@@ -2107,6 +2139,13 @@ export async function startDashboard(
21072139
if (worker.team) {
21082140
agent.team = worker.team;
21092141
}
2142+
// Extract model from spawn command (e.g., "codex --model gpt-5.2-codex" → "gpt-5.2-codex")
2143+
if (worker.cli) {
2144+
const modelMatch = worker.cli.match(/--model\s+(\S+)/);
2145+
if (modelMatch) {
2146+
agent.model = modelMatch[1];
2147+
}
2148+
}
21102149
}
21112150
}
21122151
}

packages/dashboard/src/components/AgentCard.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,14 @@ export function AgentCard({
199199
</span>
200200
)}
201201
</div>
202-
{!displayNameOverride && (
202+
{agent.cli && (
203203
<span className="text-[10px] text-text-muted truncate font-mono opacity-70 mt-0.5">
204-
{agent.isLocal ? agent.daemonName || agent.machineId : getAgentBreadcrumb(agent.name)}
204+
{agent.cli}
205205
</span>
206206
)}
207-
{agent.profile?.model && (
208-
<span className="text-[9px] text-accent-cyan font-mono opacity-80 mt-0.5" title={`Model: ${agent.profile.model}`}>
209-
{agent.profile.model}
207+
{(agent.model || agent.profile?.model) && (
208+
<span className="text-[9px] text-accent-muted font-mono opacity-80 mt-0.5" title={`Model: ${agent.model || agent.profile?.model}`}>
209+
{agent.model || agent.profile?.model}
210210
</span>
211211
)}
212212

@@ -282,12 +282,6 @@ export function AgentCard({
282282
title={statusTooltip}
283283
/>
284284
)}
285-
{agent.needsAttention && (
286-
<div
287-
className="w-2 h-2 rounded-full bg-warning animate-pulse shadow-[0_0_8px_rgba(255,107,53,0.5)]"
288-
title="Needs Attention - Agent requires user input or has pending decisions"
289-
/>
290-
)}
291285
{isStuck && (
292286
<div
293287
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-warning-light text-warning text-[10px] font-medium animate-pulse"
@@ -392,9 +386,9 @@ export function AgentCard({
392386
<div className="mt-3 flex justify-between items-center">
393387
<div className="flex gap-2 text-xs text-text-muted flex-wrap">
394388
{agent.cli && <span className="bg-bg-hover py-0.5 px-1.5 rounded">{agent.cli}</span>}
395-
{agent.profile?.model && (
396-
<span className="bg-accent-cyan/10 text-accent-cyan py-0.5 px-1.5 rounded font-mono text-[10px]" title={`Model: ${agent.profile.model}`}>
397-
{agent.profile.model}
389+
{(agent.model || agent.profile?.model) && (
390+
<span className="bg-accent-cyan/10 text-accent-cyan py-0.5 px-1.5 rounded font-mono text-[10px]" title={`Model: ${agent.model || agent.profile?.model}`}>
391+
{agent.model || agent.profile?.model}
398392
</span>
399393
)}
400394
{agent.messageCount !== undefined && agent.messageCount > 0 && (

packages/dashboard/src/components/AgentList.tsx

Lines changed: 75 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React, { useState, useMemo, useEffect, useRef } from 'react';
99
import type { Agent } from '../types';
1010
import { AgentCard } from './AgentCard';
1111
import { groupAgents, getGroupStats, filterAgents, getAgentDisplayName, type AgentGroup } from '../lib/hierarchy';
12-
import { STATUS_COLORS } from '../lib/colors';
12+
import { STATUS_COLORS, getAgentColor, getAgentInitials } from '../lib/colors';
1313

1414
export interface AgentListProps {
1515
agents: Agent[];
@@ -47,6 +47,7 @@ export function AgentList({
4747
}: AgentListProps) {
4848
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
4949
const [isPinnedExpanded, setIsPinnedExpanded] = useState(true);
50+
const [isAllCollapsed, setIsAllCollapsed] = useState(false);
5051

5152
// Filter out setup agents (temporary agents for provider auth)
5253
// and system agents like Dashboard (used for dashboard message sending)
@@ -102,14 +103,15 @@ export function AgentList({
102103
});
103104
};
104105

105-
// Derive from actual state so the label is always accurate
106-
const allExpanded = groups.length > 0 && groups.every((g) => expandedGroups.has(g.prefix));
107-
108106
const toggleAll = () => {
109-
if (allExpanded) {
110-
setExpandedGroups(new Set());
111-
} else {
107+
if (isAllCollapsed) {
108+
// Expand: restore all groups and show everything
109+
setIsAllCollapsed(false);
112110
setExpandedGroups(new Set(groups.map((g) => g.prefix)));
111+
setIsPinnedExpanded(true);
112+
} else {
113+
// Collapse: hide entire panel contents
114+
setIsAllCollapsed(true);
113115
}
114116
};
115117

@@ -133,74 +135,76 @@ export function AgentList({
133135

134136
return (
135137
<div className="flex flex-col gap-1">
136-
{groups.length > 1 && (
137-
<div className="flex justify-between items-center py-2 px-3 text-xs text-text-muted">
138-
<span>{filteredAgents.length} agents</span>
139-
<button
140-
className="bg-transparent border-none text-accent cursor-pointer text-xs hover:underline"
141-
onClick={toggleAll}
142-
>
143-
{allExpanded ? 'Collapse all' : 'Expand all'}
144-
</button>
145-
</div>
146-
)}
138+
<div className="flex justify-between items-center py-2 px-3 text-xs text-text-muted">
139+
<span>{filteredAgents.length} {filteredAgents.length === 1 ? 'agent' : 'agents'}</span>
140+
<button
141+
className="bg-transparent border-none text-accent cursor-pointer text-xs hover:underline"
142+
onClick={toggleAll}
143+
>
144+
{isAllCollapsed ? 'Expand all' : 'Collapse all'}
145+
</button>
146+
</div>
147147

148-
{/* Pinned Agents Section */}
149-
{pinnedAgentsList.length > 0 && (
150-
<div className="mb-2">
151-
<button
152-
className="flex items-center gap-2 w-full py-2 px-3 bg-transparent border-none cursor-pointer text-sm text-left rounded transition-colors duration-200 relative hover:bg-amber-400/5"
153-
onClick={() => setIsPinnedExpanded(!isPinnedExpanded)}
154-
>
155-
<div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-sm bg-amber-400" />
156-
<PinnedChevronIcon expanded={isPinnedExpanded} />
157-
<PinHeaderIcon />
158-
<span className="font-semibold text-amber-400">Pinned</span>
159-
<span className="text-text-muted font-normal">({pinnedAgentsList.length})</span>
160-
</button>
148+
{!isAllCollapsed && (
149+
<>
150+
{/* Pinned Agents Section */}
151+
{pinnedAgentsList.length > 0 && (
152+
<div className="mb-2">
153+
<button
154+
className="flex items-center gap-2 w-full py-2 px-3 bg-transparent border-none cursor-pointer text-sm text-left rounded transition-colors duration-200 relative hover:bg-amber-400/5"
155+
onClick={() => setIsPinnedExpanded(!isPinnedExpanded)}
156+
>
157+
<div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-sm bg-amber-400" />
158+
<PinnedChevronIcon expanded={isPinnedExpanded} />
159+
<PinHeaderIcon />
160+
<span className="font-semibold text-amber-400">Pinned</span>
161+
<span className="text-text-muted font-normal">({pinnedAgentsList.length})</span>
162+
</button>
161163

162-
{isPinnedExpanded && (
163-
<div className="py-1 pl-4 flex flex-col gap-1">
164-
{pinnedAgentsList.map((agent) => (
165-
<AgentCard
166-
key={agent.name}
167-
agent={agent}
168-
isSelected={agent.name === selectedAgent}
169-
compact={compact}
170-
isPinned={true}
171-
isMaxPinned={isMaxPinned}
172-
onClick={onAgentSelect}
173-
onMessageClick={onAgentMessage}
174-
onReleaseClick={onReleaseClick}
175-
onLogsClick={onLogsClick}
176-
onProfileClick={onProfileClick}
177-
onPinToggle={onPinToggle}
178-
/>
179-
))}
164+
{isPinnedExpanded && (
165+
<div className="py-1 pl-4 flex flex-col gap-1">
166+
{pinnedAgentsList.map((agent) => (
167+
<AgentCard
168+
key={agent.name}
169+
agent={agent}
170+
isSelected={agent.name === selectedAgent}
171+
compact={compact}
172+
isPinned={true}
173+
isMaxPinned={isMaxPinned}
174+
onClick={onAgentSelect}
175+
onMessageClick={onAgentMessage}
176+
onReleaseClick={onReleaseClick}
177+
onLogsClick={onLogsClick}
178+
onProfileClick={onProfileClick}
179+
onPinToggle={onPinToggle}
180+
/>
181+
))}
182+
</div>
183+
)}
180184
</div>
181185
)}
182-
</div>
183-
)}
184186

185-
{groups.map((group) => (
186-
<AgentGroupComponent
187-
key={group.prefix}
188-
group={group}
189-
isExpanded={expandedGroups.has(group.prefix)}
190-
selectedAgent={selectedAgent}
191-
compact={compact}
192-
showStats={showGroupStats}
193-
pinnedAgents={pinnedAgents}
194-
isMaxPinned={isMaxPinned}
195-
onToggle={() => toggleGroup(group.prefix)}
196-
onAgentSelect={onAgentSelect}
197-
onAgentMessage={onAgentMessage}
198-
onReleaseClick={onReleaseClick}
199-
onLogsClick={onLogsClick}
200-
onProfileClick={onProfileClick}
201-
onPinToggle={onPinToggle}
202-
/>
203-
))}
187+
{groups.map((group) => (
188+
<AgentGroupComponent
189+
key={group.prefix}
190+
group={group}
191+
isExpanded={expandedGroups.has(group.prefix)}
192+
selectedAgent={selectedAgent}
193+
compact={compact}
194+
showStats={showGroupStats}
195+
pinnedAgents={pinnedAgents}
196+
isMaxPinned={isMaxPinned}
197+
onToggle={() => toggleGroup(group.prefix)}
198+
onAgentSelect={onAgentSelect}
199+
onAgentMessage={onAgentMessage}
200+
onReleaseClick={onReleaseClick}
201+
onLogsClick={onLogsClick}
202+
onProfileClick={onProfileClick}
203+
onPinToggle={onPinToggle}
204+
/>
205+
))}
206+
</>
207+
)}
204208
</div>
205209
);
206210
}
@@ -241,13 +245,11 @@ function AgentGroupComponent({
241245
const stats = showStats ? getGroupStats(group.agents) : null;
242246

243247
// Check if this is a "solo" agent - single agent in group where name matches prefix
244-
// (e.g., "Lead" agent with no team set creates a "lead" group)
245248
const isSoloAgent =
246249
group.agents.length === 1 &&
247250
group.agents[0].name.toLowerCase() === group.prefix.toLowerCase();
248251

249252
// For solo agents, render just the card without a group header
250-
// When collapsed, switch to compact mode since there's no group header to collapse to
251253
if (isSoloAgent) {
252254
const agent = group.agents[0];
253255
return (
@@ -256,7 +258,7 @@ function AgentGroupComponent({
256258
key={agent.name}
257259
agent={agent}
258260
isSelected={agent.name === selectedAgent}
259-
compact={!isExpanded || compact}
261+
compact={compact}
260262
isPinned={pinnedAgents.includes(agent.name)}
261263
isMaxPinned={isMaxPinned}
262264
onClick={onAgentSelect}

0 commit comments

Comments
 (0)