Compare commits
2 Commits
feat/ms22-
...
chore/ms22
| Author | SHA1 | Date | |
|---|---|---|---|
| a81668d4fe | |||
| a70f149886 |
128
apps/web/src/components/chat/AgentSelector.tsx
Normal file
128
apps/web/src/components/chat/AgentSelector.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface AgentSelectorProps {
|
||||
selectedAgent?: string | null;
|
||||
onChange?: (agent: string | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AGENT_CONFIG = {
|
||||
jarvis: {
|
||||
displayName: "Jarvis",
|
||||
role: "Orchestrator",
|
||||
color: "#3498db",
|
||||
},
|
||||
builder: {
|
||||
displayName: "Builder",
|
||||
role: "Coding Agent",
|
||||
color: "#3b82f6",
|
||||
},
|
||||
medic: {
|
||||
displayName: "Medic",
|
||||
role: "Health Monitor",
|
||||
color: "#10b981",
|
||||
},
|
||||
} as const;
|
||||
|
||||
function JarvisIcon({ className }: { className?: string }): React.ReactElement {
|
||||
return (
|
||||
<svg
|
||||
className={`w-3 h-3 ${className ?? ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 2v4M12 22v-4" />
|
||||
<path d="M2 12h4M22 12h-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BuilderIcon({ className }: { className?: string }): React.ReactElement {
|
||||
return (
|
||||
<svg
|
||||
className={`w-3 h-3 ${className ?? ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MedicIcon({ className }: { className?: string }): React.ReactElement {
|
||||
return (
|
||||
<svg
|
||||
className={`w-3 h-3 ${className ?? ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const AGENT_ICONS: Record<string, React.FC<{ className?: string }>> = {
|
||||
jarvis: JarvisIcon,
|
||||
builder: BuilderIcon,
|
||||
medic: MedicIcon,
|
||||
};
|
||||
|
||||
export function AgentSelector({
|
||||
selectedAgent,
|
||||
onChange,
|
||||
disabled,
|
||||
}: AgentSelectorProps): React.ReactElement {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium" style={{ color: "rgb(var(--text-muted))" }}>
|
||||
Agent
|
||||
</span>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Object.entries(AGENT_CONFIG).map(([name, config]) => {
|
||||
const Icon = AGENT_ICONS[name];
|
||||
const isSelected = selectedAgent === name;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
onClick={() => onChange?.(isSelected ? null : name)}
|
||||
disabled={disabled}
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg border transition-all text-xs ${
|
||||
isSelected ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
||||
} ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
style={{
|
||||
borderColor: isSelected
|
||||
? "rgb(var(--accent-primary))"
|
||||
: "rgb(var(--border-default))",
|
||||
color: isSelected ? "rgb(var(--accent-primary))" : "rgb(var(--text-primary))",
|
||||
}}
|
||||
title={`${config.displayName} — ${config.role}`}
|
||||
>
|
||||
<span
|
||||
className="rounded-full"
|
||||
style={{
|
||||
backgroundColor: config.color,
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
}}
|
||||
/>
|
||||
{Icon && <Icon />}
|
||||
<span className="font-medium">{config.displayName}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { useWorkspaceId } from "@/lib/hooks";
|
||||
import { MessageList } from "./MessageList";
|
||||
import { ChatInput, type ModelId, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS } from "./ChatInput";
|
||||
import { ChatEmptyState } from "./ChatEmptyState";
|
||||
import { AgentSelector } from "./AgentSelector";
|
||||
import type { Message } from "@/hooks/useChat";
|
||||
|
||||
export interface ChatRef {
|
||||
@@ -66,6 +67,7 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
|
||||
const [selectedModel, setSelectedModel] = useState<ModelId>("llama3.2");
|
||||
const [temperature, setTemperature] = useState<number>(DEFAULT_TEMPERATURE);
|
||||
const [maxTokens, setMaxTokens] = useState<number>(DEFAULT_MAX_TOKENS);
|
||||
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
||||
|
||||
// Suggestion fill value: controls ChatInput's textarea content
|
||||
const [suggestionValue, setSuggestionValue] = useState<string | undefined>(undefined);
|
||||
@@ -88,6 +90,7 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
|
||||
temperature,
|
||||
maxTokens,
|
||||
...(initialProjectId !== undefined && { projectId: initialProjectId }),
|
||||
...(selectedAgent !== null && { agent: selectedAgent }),
|
||||
});
|
||||
|
||||
// Read workspace ID from localStorage (set by auth-context after session check).
|
||||
@@ -375,6 +378,13 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto max-w-4xl px-4 py-4 lg:px-8">
|
||||
<div className="mb-3">
|
||||
<AgentSelector
|
||||
selectedAgent={selectedAgent}
|
||||
onChange={setSelectedAgent}
|
||||
disabled={isChatLoading || isStreaming || !user}
|
||||
/>
|
||||
</div>
|
||||
<ChatInput
|
||||
onSend={handleSendMessage}
|
||||
disabled={isChatLoading || !user}
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface UseChatOptions {
|
||||
maxTokens?: number;
|
||||
systemPrompt?: string;
|
||||
projectId?: string | null;
|
||||
agent?: string;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
@@ -63,6 +64,7 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
||||
maxTokens,
|
||||
systemPrompt,
|
||||
projectId,
|
||||
agent,
|
||||
onError,
|
||||
} = options;
|
||||
|
||||
@@ -77,6 +79,10 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
||||
const projectIdRef = useRef<string | null>(projectId ?? null);
|
||||
projectIdRef.current = projectId ?? null;
|
||||
|
||||
// Track agent in ref to prevent stale closures
|
||||
const agentRef = useRef<string | undefined>(agent);
|
||||
agentRef.current = agent;
|
||||
|
||||
// Track messages in ref to prevent stale closures during rapid sends
|
||||
const messagesRef = useRef<Message[]>(messages);
|
||||
messagesRef.current = messages;
|
||||
@@ -209,6 +215,7 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
||||
...(temperature !== undefined && { temperature }),
|
||||
...(maxTokens !== undefined && { maxTokens }),
|
||||
...(systemPrompt !== undefined && { systemPrompt }),
|
||||
...(agentRef.current && { agent: agentRef.current }),
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
125
apps/web/src/lib/api/agents.ts
Normal file
125
apps/web/src/lib/api/agents.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Agent API client
|
||||
* Handles agent-related API interactions
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from "./client";
|
||||
|
||||
export interface AgentStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
role: string;
|
||||
isActive: boolean;
|
||||
containerStatus?: "running" | "stopped" | "unknown";
|
||||
}
|
||||
|
||||
export interface UserAgent {
|
||||
id: string;
|
||||
userId: string;
|
||||
templateId: string | null;
|
||||
name: string;
|
||||
displayName: string;
|
||||
role: string;
|
||||
personality: string;
|
||||
primaryModel: string | null;
|
||||
fallbackModels: string[];
|
||||
toolPermissions: string[];
|
||||
discordChannel: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateUserAgentRequest {
|
||||
templateId?: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
role: string;
|
||||
personality: string;
|
||||
primaryModel?: string;
|
||||
fallbackModels?: string[];
|
||||
toolPermissions?: string[];
|
||||
discordChannel?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateUserAgentRequest {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
role?: string;
|
||||
personality?: string;
|
||||
primaryModel?: string;
|
||||
fallbackModels?: string[];
|
||||
toolPermissions?: string[];
|
||||
discordChannel?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateUserAgentRequest {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
role?: string;
|
||||
personality?: string;
|
||||
primaryModel?: string;
|
||||
fallbackModels?: string[];
|
||||
toolPermissions?: string[];
|
||||
discordChannel?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all user's agents
|
||||
*/
|
||||
export async function getAgents(): Promise<UserAgent[]> {
|
||||
return apiGet<UserAgent[]>("/api/agents");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all agent statuses
|
||||
*/
|
||||
export async function getAgentStatuses(): Promise<AgentStatus[]> {
|
||||
return apiGet<AgentStatus[]>("/api/agents/status");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single agent by ID
|
||||
*/
|
||||
export async function getAgent(id: string): Promise<UserAgent> {
|
||||
return apiGet<UserAgent>(`/api/agents/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single agent's status
|
||||
*/
|
||||
export async function getAgentStatus(id: string): Promise<AgentStatus> {
|
||||
return apiGet<AgentStatus>(`/api/agents/${id}/status`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new custom agent
|
||||
*/
|
||||
export async function createAgent(data: CreateUserAgentRequest): Promise<UserAgent> {
|
||||
return apiPost<UserAgent>("/api/agents", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an agent from a template
|
||||
*/
|
||||
export async function createAgentFromTemplate(templateId: string): Promise<UserAgent> {
|
||||
return apiPost<UserAgent>(`/api/agents/from-template/${templateId}`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an agent
|
||||
*/
|
||||
export async function updateAgent(id: string, data: UpdateUserAgentRequest): Promise<UserAgent> {
|
||||
return apiPatch<UserAgent>(`/api/agents/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an agent
|
||||
*/
|
||||
export async function deleteAgent(id: string): Promise<void> {
|
||||
await apiDelete(`/api/agents/${id}`);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export interface ChatRequest {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
systemPrompt?: string;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
@@ -117,7 +118,11 @@ export function streamGuestChat(
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ messages: request.messages, stream: true }),
|
||||
body: JSON.stringify({
|
||||
messages: request.messages,
|
||||
stream: true,
|
||||
...(request.agent && { agent: request.agent }),
|
||||
}),
|
||||
signal: signal ?? null,
|
||||
});
|
||||
|
||||
@@ -269,7 +274,11 @@ export function streamChatMessage(
|
||||
"X-CSRF-Token": csrfToken,
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ messages: request.messages, stream: true }),
|
||||
body: JSON.stringify({
|
||||
messages: request.messages,
|
||||
stream: true,
|
||||
...(request.agent && { agent: request.agent }),
|
||||
}),
|
||||
signal: signal ?? null,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
## Mission
|
||||
|
||||
**ID:** ms22-p2-named-agent-fleet-20260304
|
||||
**Statement:** Implement named agent fleet (jarvis, builder, medic) with per-agent personalities, model assignments, Discord channel routing, and WebUI selector.
|
||||
**PRD:** `docs/PRD-MS22-P2-AGENT-FLEET.md`
|
||||
**Phase:** Execution
|
||||
**Status:** in-progress
|
||||
**Last Updated:** 2026-03-04
|
||||
**ID:** ms22-p2-named-agent-fleet-20260304
|
||||
**Statement:** Implement named agent fleet (jarvis, builder, medic) with per-agent personalities, model assignments, Discord channel routing, and WebUI selector.
|
||||
**PRD:** `docs/PRD-MS22-P2-AGENT-FLEET.md`
|
||||
**Phase:** Execution
|
||||
**Status:** in-progress
|
||||
**Last Updated:** 2026-03-05
|
||||
|
||||
## Success Criteria
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
| 1 | schema-seed | Schema+Seed | ✅ done | P2-001, P2-002 | PRs #675, #677 merged |
|
||||
| 2 | admin-crud | Admin CRUD | ✅ done | P2-003 | PR #678 merged |
|
||||
| 3 | user-crud | User CRUD | ✅ done | P2-004 | PR #682 merged |
|
||||
| 4 | agent-routing | Agent Routing | ⬜ pending | P2-005, P2-006 | Depends on M3 |
|
||||
| 5 | discord-ui | Discord+UI | ⬜ pending | P2-007, P2-008 | Depends on M4 |
|
||||
| 4 | agent-routing | Agent Routing | ✅ done | P2-005, P2-006 | PR #684 merged |
|
||||
| 5 | discord-ui | Discord+UI | 🔄 partial | P2-007, P2-008 | P2-008 done (PR #685) |
|
||||
| 6 | verification | Verification | ⬜ pending | P2-009, P2-010 | Final gate |
|
||||
|
||||
## Task Summary
|
||||
@@ -43,26 +43,27 @@ See `docs/TASKS.md` — MS22 Phase 2 section for full task details.
|
||||
| P2-002 Seed | ✅ done | #677 | jarvis/builder/medic templates |
|
||||
| P2-003 Admin CRUD | ✅ done | #678 | /admin/agent-templates |
|
||||
| P2-004 User CRUD | ✅ done | #682 | /api/agents |
|
||||
| P2-005 Status endpoints | ⬜ not-started | — | |
|
||||
| P2-006 Chat routing | ⬜ not-started | — | |
|
||||
| P2-005 Status endpoints | ✅ done | #684 | Agent status API |
|
||||
| P2-006 Chat routing | ✅ done | #684 | Agent routing in chat proxy |
|
||||
| P2-007 Discord routing | ⬜ not-started | — | |
|
||||
| P2-008 WebUI selector | ⬜ not-started | — | |
|
||||
| P2-008 WebUI selector | ✅ done | #685 | AgentSelector component |
|
||||
| P2-009 Unit tests | ⬜ not-started | — | |
|
||||
| P2-010 E2E verification | ⬜ not-started | — | |
|
||||
|
||||
## Token Budget
|
||||
|
||||
| Phase | Est | Used |
|
||||
| ----------------- | -------- | -------------------- |
|
||||
| Schema+Seed+CRUD | 30K | ~15K (done directly) |
|
||||
| User CRUD+Routing | 40K | ~25K |
|
||||
| Discord+UI | 30K | — |
|
||||
| Verification | 10K | — |
|
||||
| **Total** | **110K** | **~40K** |
|
||||
| Phase | Est | Used |
|
||||
| ----------------- | -------- | ------------------ |
|
||||
| Schema+Seed+CRUD | 30K | ~15K |
|
||||
| User CRUD+Routing | 40K | ~25K |
|
||||
| Discord+UI | 30K | ~15K (P2-008 done) |
|
||||
| Verification | 10K | — |
|
||||
| **Total** | **110K** | **~55K** |
|
||||
|
||||
## Session Log
|
||||
|
||||
| Date | Work Done |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| 2026-03-04 | Session 2: Fixed CI security audit, merged PRs #681, #678, #682. Milestones 1-3 complete (4/6 remaining). |
|
||||
| 2026-03-04 | P2-001..003 shipped; CI fix; postgres rebuilt; mission initialized |
|
||||
| Date | Work Done |
|
||||
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| 2026-03-05 | Session 3: Completed P2-008 (WebUI agent selector) PR #685. Milestones 1-4 + P2-008 complete (3 tasks remaining). |
|
||||
| 2026-03-04 | Session 2: Fixed CI security audit, merged PRs #681, #678, #682. Milestones 1-3 complete (4/6 remaining). |
|
||||
| 2026-03-04 | P2-001..003 shipped; CI fix; postgres rebuilt; mission initialized |
|
||||
|
||||
@@ -100,9 +100,9 @@ PRD: `docs/PRD-MS22-P2-AGENT-FLEET.md`
|
||||
| MS22-P2-002 | done | p2-fleet | Seed default agents (jarvis, builder, medic) | TASKS:P2 | api | feat/ms22-p2-agent-seed | P2-001 | P2-004 | orchestrator | 2026-03-04 | 2026-03-04 | 5K | 2K | PR #677 merged |
|
||||
| MS22-P2-003 | done | p2-fleet | Agent template CRUD endpoints (admin) | TASKS:P2 | api | feat/ms22-p2-agent-crud | P2-001 | P2-005 | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 5K | PR #678 merged |
|
||||
| MS22-P2-004 | done | p2-fleet | User agent CRUD endpoints | TASKS:P2 | api | feat/ms22-p2-user-agents | P2-002,P2-003 | P2-006 | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 8K | PR #682 merged |
|
||||
| MS22-P2-005 | in-progress | p2-fleet | Agent status endpoints | TASKS:P2 | api | feat/ms22-p2-agent-status | P2-003 | P2-008 | orchestrator | 2026-03-04 | — | 10K | — | |
|
||||
| MS22-P2-006 | not-started | p2-fleet | Agent chat routing (select agent by name) | TASKS:P2 | api | feat/ms22-p2-agent-routing | P2-004 | P2-007 | — | — | — | 15K | — | |
|
||||
| MS22-P2-005 | done | p2-fleet | Agent status endpoints | TASKS:P2 | api | feat/ms22-p2-agent-routing | P2-003 | P2-008 | orchestrator | 2026-03-04 | 2026-03-04 | 10K | 5K | PR #684 merged |
|
||||
| MS22-P2-006 | done | p2-fleet | Agent chat routing (select agent by name) | TASKS:P2 | api | feat/ms22-p2-agent-routing | P2-004 | P2-007 | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 5K | PR #684 merged |
|
||||
| MS22-P2-007 | not-started | p2-fleet | Discord channel → agent routing | TASKS:P2 | api | feat/ms22-p2-discord-router | P2-006 | P2-009 | — | — | — | 15K | — | |
|
||||
| MS22-P2-008 | not-started | p2-fleet | Agent list/selector UI in WebUI | TASKS:P2 | web | feat/ms22-p2-agent-ui | P2-005 | — | — | — | — | 15K | — | |
|
||||
| MS22-P2-008 | done | p2-fleet | Agent list/selector UI in WebUI | TASKS:P2 | web | feat/ms22-p2-agent-ui | P2-005 | — | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 8K | PR #685 merged |
|
||||
| MS22-P2-009 | not-started | p2-fleet | Unit tests for agent services | TASKS:P2 | api | test/ms22-p2-agent-tests | P2-007 | P2-010 | — | — | — | 15K | — | |
|
||||
| MS22-P2-010 | not-started | p2-fleet | E2E verification: Discord → agent → response | TASKS:P2 | stack | — | P2-009 | — | — | — | — | 10K | — | |
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
|
||||
## Session Log
|
||||
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
| ------- | ---------- | --------- | ---------------------- | ------------------------------------------------------------------------------ |
|
||||
| 2 | 2026-03-04 | M1+M2+M3 | P2-004 done | Fixed CI security audit, merged PRs #681, #678, #682. Milestones 1-3 complete. |
|
||||
| 1 | 2026-03-04 | M1+M2 | P2-001, P2-002, P2-003 | Schema, seed, and Admin CRUD complete |
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
| ------- | ---------- | --------- | ---------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| 3 | 2026-03-05 | M4+M5 | P2-008 done | Fixed corrupted AgentSelector.tsx, integrated into Chat.tsx. PR #685 merged. 8/10 tasks done. |
|
||||
| 2 | 2026-03-04 | M1+M2+M3 | P2-004 done | Fixed CI security audit, merged PRs #681, #678, #682. Milestones 1-3 complete. |
|
||||
| 1 | 2026-03-04 | M1+M2 | P2-001, P2-002, P2-003 | Schema, seed, and Admin CRUD complete |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user