Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions frontend/src/components/agents/AgentsHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,10 @@ const AgentsHub = ({ currentPodId: propPodId = null }) => {
if (!dmPodId) {
throw new Error('DM pod not returned');
}
navigate(`/pods/agent-admin/${dmPodId}`);
// Stay in the shell that launched this — a v2 card must not eject the
// user into the legacy ChatRoom. Mirrors the install handoff above.
const isV2Context = typeof window !== 'undefined' && window.location.pathname.startsWith('/v2');
navigate(isV2Context ? `/v2/pods/${dmPodId}` : `/pods/agent-admin/${dmPodId}`);
} catch (err) {
setError(err.response?.data?.message || 'Failed to open agent DM');
}
Expand All @@ -1352,7 +1355,10 @@ const AgentsHub = ({ currentPodId: propPodId = null }) => {
if (!roomPodId) {
throw new Error('Agent room not returned');
}
navigate(`/pods/agent-room/${roomPodId}`);
// Stay in the shell that launched this — a v2 card must not eject the
// user into the legacy ChatRoom. Mirrors the install handoff above.
const isV2Context = typeof window !== 'undefined' && window.location.pathname.startsWith('/v2');
navigate(isV2Context ? `/v2/pods/${roomPodId}` : `/pods/agent-room/${roomPodId}`);
} catch (err) {
setError(err.response?.data?.message || 'Failed to open agent room');
}
Expand Down
43 changes: 42 additions & 1 deletion frontend/src/v2/components/V2YourTeamPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ const V2YourTeamPage: React.FC = () => {
// from the union of categories present on loaded agents so a sparse team
// doesn't render empty filters.
const [filter, setFilter] = useState<string>('all');
// Key (`name:instanceId`) of the agent whose 1:1 room is currently opening,
// so its "Talk to" button can show progress and block a double-submit.
const [opening, setOpening] = useState<string | null>(null);

useEffect(() => {
let cancelled = false;
Expand Down Expand Up @@ -144,6 +147,33 @@ const V2YourTeamPage: React.FC = () => {
: sortedAgents.filter((a) => (a.category || 'Uncategorized') === filter)
), [sortedAgents, filter]);

// Open the coached 1:1 (agent-room) for this agent — the same surface the
// post-install handoff lands on. Without this, "talk to your agent" is only
// reachable from the project pod (a group), so the 1:1 relationship a user
// forms at install has no entry point on their primary team surface.
const handleTalkTo = async (a: AgentInstallationSummary, e: React.MouseEvent) => {
e.stopPropagation();
const key = `${a.name}:${a.instanceId}`;
setOpening(key);
setError(null);
try {
const token = localStorage.getItem('token');
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
const res = await axios.post<{ room?: { _id?: string } }>(
'/api/agents/runtime/room',
{ agentName: a.name, instanceId: a.instanceId || 'default', podId: a.podId || undefined },
{ headers },
);
const roomId = res.data?.room?._id;
if (!roomId) throw new Error('Agent room not returned');
navigate(`/v2/pods/${roomId}`);
} catch (err: unknown) {
const resp = (err as { response?: { data?: { message?: string } } })?.response;
setError(resp?.data?.message || 'Could not open a 1:1 with this agent — try again.');
setOpening(null);
}
};

return (
<div className="v2-team">
<header className="v2-team__header">
Expand Down Expand Up @@ -221,12 +251,14 @@ const V2YourTeamPage: React.FC = () => {
const podLabel = a.podName || 'Untitled project';
const runtimeLabel = formatRuntime(a);
const lastSeen = formatRelative(a.lastHeartbeatAt);
const cardKey = `${a.name}:${a.instanceId}`;
const isOpening = opening === cardKey;
const onCardClick = () => {
if (a.podId) navigate(`/v2/pods/${a.podId}`);
};
return (
<article
key={`${a.name}:${a.instanceId}`}
key={cardKey}
className="v2-team-card"
onClick={onCardClick}
role="button"
Expand All @@ -253,6 +285,15 @@ const V2YourTeamPage: React.FC = () => {
{lastSeen}
</div>
</div>
<button
type="button"
className="v2-team-card__talk"
onClick={(e) => handleTalkTo(a, e)}
disabled={isOpening}
aria-label={`Talk to ${display} one-to-one`}
>
{isOpening ? 'Opening…' : 'Talk to'}
</button>
</article>
);
})}
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/v2/v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -4359,6 +4359,34 @@
background: #25c281;
}

/* Specificity-matched against the global .v2-root button:not(.MuiButtonBase-root)
reset (0,0,2,1) — prefix with .v2-root button.X so source order wins. Same
pattern as .v2-chat__send / .v2-pods__item--active. */
.v2-root button.v2-team-card__talk {
align-self: center;
flex-shrink: 0;
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 14px;
font-size: 13px;
font-weight: 600;
color: var(--v2-accent);
background: transparent;
border: 1px solid var(--v2-accent);
border-radius: var(--v2-radius-sm);
white-space: nowrap;
transition: background 80ms ease, color 80ms ease;
}
.v2-root button.v2-team-card__talk:hover {
background: var(--v2-accent);
color: #fff;
}
.v2-root button.v2-team-card__talk:disabled {
opacity: 0.6;
cursor: default;
}

@media (max-width: 760px) {
.v2-team { padding: 20px 16px 48px; }
.v2-team__header { flex-direction: column; }
Expand Down
Loading