feat(web): add orchestrator command system in chat interface (#521)
All checks were successful
ci/woodpecker/push/web Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #521.
This commit is contained in:
2026-02-26 03:39:00 +00:00
committed by jason.woltje
parent 859dcfc4b7
commit b110c469c4
6 changed files with 1119 additions and 8 deletions

View File

@@ -0,0 +1,293 @@
/**
* Tests for useOrchestratorCommands hook
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useOrchestratorCommands } from "./useOrchestratorCommands";
import type { Message } from "./useChat";
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
function makeOkResponse(data: unknown): Response {
return {
ok: true,
status: 200,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
} as unknown as Response;
}
/** Run executeCommand and return the result synchronously after act() */
async function runCommand(
executeCommand: (content: string) => Promise<Message | null>,
content: string
): Promise<Message | null> {
let msg: Message | null = null;
await act(async () => {
msg = await executeCommand(content);
});
return msg;
}
describe("useOrchestratorCommands", () => {
beforeEach(() => {
mockFetch.mockReset();
});
describe("isCommand", () => {
it("returns true for messages starting with /", () => {
const { result } = renderHook(() => useOrchestratorCommands());
expect(result.current.isCommand("/status")).toBe(true);
expect(result.current.isCommand("/agents")).toBe(true);
expect(result.current.isCommand("/help")).toBe(true);
expect(result.current.isCommand(" /status")).toBe(true);
});
it("returns false for regular messages", () => {
const { result } = renderHook(() => useOrchestratorCommands());
expect(result.current.isCommand("hello")).toBe(false);
expect(result.current.isCommand("tell me about /status")).toBe(false);
expect(result.current.isCommand("")).toBe(false);
});
});
describe("executeCommand", () => {
describe("/help", () => {
it("returns help message without network calls", async () => {
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/help");
expect(mockFetch).not.toHaveBeenCalled();
expect(msg).not.toBeNull();
expect(msg?.role).toBe("assistant");
expect(msg?.content).toContain("/status");
expect(msg?.content).toContain("/agents");
expect(msg?.content).toContain("/jobs");
expect(msg?.content).toContain("/pause");
expect(msg?.content).toContain("/resume");
});
it("returns message with id and createdAt", async () => {
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/help");
expect(msg?.id).toBeDefined();
expect(msg?.createdAt).toBeDefined();
});
});
describe("/status", () => {
it("calls /api/orchestrator/health and returns formatted status", async () => {
mockFetch.mockResolvedValueOnce(
makeOkResponse({ status: "ready", version: "1.2.3", uptime: 3661 })
);
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/status");
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/health", { method: "GET" });
expect(msg?.role).toBe("assistant");
expect(msg?.content).toContain("Ready");
expect(msg?.content).toContain("1.2.3");
expect(msg?.content).toContain("1h");
});
it("shows Not Ready when status is not ready", async () => {
mockFetch.mockResolvedValueOnce(makeOkResponse({ status: "not-ready" }));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/status");
expect(msg?.content).toContain("Not Ready");
});
it("handles network error gracefully", async () => {
mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/status");
expect(msg?.role).toBe("assistant");
expect(msg?.content).toContain("Error");
expect(msg?.content).toContain("Connection refused");
});
it("shows error from API response", async () => {
mockFetch.mockResolvedValueOnce(
makeOkResponse({ error: "ORCHESTRATOR_API_KEY is not configured" })
);
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/status");
expect(msg?.content).toContain("Not reachable");
});
});
describe("/agents", () => {
it("calls /api/orchestrator/agents and returns agent table", async () => {
const agents = [
{ id: "agent-1", status: "active", type: "codex", startedAt: "2026-02-25T10:00:00Z" },
{
id: "agent-2",
agentStatus: "TERMINATED",
channel: "claude",
startedAt: "2026-02-25T09:00:00Z",
},
];
mockFetch.mockResolvedValueOnce(makeOkResponse(agents));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/agents");
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/agents", { method: "GET" });
expect(msg?.content).toContain("agent-1");
expect(msg?.content).toContain("agent-2");
expect(msg?.content).toContain("TERMINATED");
});
it("handles empty agent list", async () => {
mockFetch.mockResolvedValueOnce(makeOkResponse([]));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/agents");
expect(msg?.content).toContain("No agents currently running");
});
it("handles agents in nested object", async () => {
const data = {
agents: [{ id: "agent-nested", status: "active" }],
};
mockFetch.mockResolvedValueOnce(makeOkResponse(data));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/agents");
expect(msg?.content).toContain("agent-nested");
});
it("handles network error gracefully", async () => {
mockFetch.mockRejectedValueOnce(new Error("Timeout"));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/agents");
expect(msg?.content).toContain("Error");
expect(msg?.content).toContain("Timeout");
});
});
describe("/jobs", () => {
it("calls /api/orchestrator/queue/stats", async () => {
mockFetch.mockResolvedValueOnce(
makeOkResponse({ pending: 3, active: 1, completed: 42, failed: 0 })
);
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/jobs");
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/stats", { method: "GET" });
expect(msg?.content).toContain("3");
expect(msg?.content).toContain("42");
expect(msg?.content).toContain("Pending");
expect(msg?.content).toContain("Completed");
});
it("/queue is an alias for /jobs", async () => {
mockFetch.mockResolvedValueOnce(makeOkResponse({ pending: 0, active: 0 }));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/queue");
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/stats", { method: "GET" });
expect(msg?.role).toBe("assistant");
});
it("shows paused indicator when queue is paused", async () => {
mockFetch.mockResolvedValueOnce(makeOkResponse({ pending: 0, active: 0, paused: true }));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/jobs");
expect(msg?.content).toContain("paused");
});
});
describe("/pause", () => {
it("calls POST /api/orchestrator/queue/pause", async () => {
mockFetch.mockResolvedValueOnce(
makeOkResponse({ success: true, message: "Queue paused." })
);
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/pause");
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/pause", {
method: "POST",
});
expect(msg?.content).toContain("paused");
});
it("handles API error response", async () => {
mockFetch.mockResolvedValueOnce(makeOkResponse({ error: "Already paused." }));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/pause");
expect(msg?.content).toContain("failed");
expect(msg?.content).toContain("Already paused");
});
it("handles network error", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network failure"));
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/pause");
expect(msg?.content).toContain("Error");
});
});
describe("/resume", () => {
it("calls POST /api/orchestrator/queue/resume", async () => {
mockFetch.mockResolvedValueOnce(
makeOkResponse({ success: true, message: "Queue resumed." })
);
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/resume");
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/resume", {
method: "POST",
});
expect(msg?.content).toContain("resumed");
});
});
describe("unknown command", () => {
it("returns help hint for unknown commands", async () => {
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "/unknown-command");
expect(mockFetch).not.toHaveBeenCalled();
expect(msg?.content).toContain("Unknown command");
expect(msg?.content).toContain("/unknown-command");
expect(msg?.content).toContain("/help");
});
});
describe("non-command input", () => {
it("returns null for regular messages", async () => {
const { result } = renderHook(() => useOrchestratorCommands());
const msg = await runCommand(result.current.executeCommand, "hello world");
expect(msg).toBeNull();
expect(mockFetch).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,356 @@
/**
* useOrchestratorCommands hook
*
* Parses chat messages for `/command` prefixes and routes them to the
* orchestrator proxy API routes instead of the LLM.
*
* Supported commands:
* /status — GET /api/orchestrator/health
* /agents — GET /api/orchestrator/agents
* /jobs — GET /api/orchestrator/queue/stats
* /queue — alias for /jobs
* /pause — POST /api/orchestrator/queue/pause
* /resume — POST /api/orchestrator/queue/resume
* /help — Display available commands locally (no API call)
*/
import { useCallback } from "react";
import type { Message } from "@/hooks/useChat";
// ---------------------------------------------------------------------------
// Command definitions
// ---------------------------------------------------------------------------
export interface OrchestratorCommand {
name: string;
description: string;
aliases?: string[];
}
export const ORCHESTRATOR_COMMANDS: OrchestratorCommand[] = [
{ name: "/status", description: "Show orchestrator health and status" },
{ name: "/agents", description: "List all running agents" },
{ name: "/jobs", description: "Show queue statistics", aliases: ["/queue"] },
{ name: "/pause", description: "Pause the job queue" },
{ name: "/resume", description: "Resume the job queue" },
{ name: "/help", description: "Show available commands" },
];
// ---------------------------------------------------------------------------
// API response shapes (loosely typed — orchestrator may vary)
// ---------------------------------------------------------------------------
interface HealthResponse {
status?: string;
version?: string;
uptime?: number;
ready?: boolean;
error?: string;
}
interface Agent {
id?: string;
sessionKey?: string;
status?: string;
type?: string;
agentStatus?: string;
startedAt?: string;
label?: string;
channel?: string;
}
interface AgentsResponse {
agents?: Agent[];
error?: string;
}
interface QueueStats {
pending?: number;
active?: number;
completed?: number;
failed?: number;
waiting?: number;
delayed?: number;
paused?: boolean;
error?: string;
}
interface ActionResponse {
success?: boolean;
message?: string;
status?: string;
error?: string;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function makeId(): string {
return `orch-${Date.now().toString()}-${Math.random().toString(36).slice(2, 8)}`;
}
function makeMessage(content: string): Message {
return {
id: makeId(),
role: "assistant",
content,
createdAt: new Date().toISOString(),
};
}
function errorMessage(command: string, detail: string): Message {
return makeMessage(
`**Error running \`${command}\`**\n\n${detail}\n\n_Check that the orchestrator is running and the API key is configured._`
);
}
// ---------------------------------------------------------------------------
// Formatters
// ---------------------------------------------------------------------------
function formatStatus(data: HealthResponse): string {
if (data.error) {
return `**Orchestrator Status**\n\nStatus: Not reachable\n\nError: ${data.error}`;
}
const statusLabel = data.status ?? (data.ready === true ? "ready" : "unknown");
const isReady =
statusLabel === "ready" ||
statusLabel === "ok" ||
statusLabel === "healthy" ||
data.ready === true;
const badge = isReady ? "Ready" : "Not Ready";
const lines: string[] = [
`**Orchestrator Status**\n`,
`| Field | Value |`,
`|---|---|`,
`| Status | **${badge}** |`,
];
if (data.version != null) {
lines.push(`| Version | \`${data.version}\` |`);
}
if (data.uptime != null) {
const uptimeSec = Math.floor(data.uptime);
const hours = Math.floor(uptimeSec / 3600);
const mins = Math.floor((uptimeSec % 3600) / 60);
const secs = uptimeSec % 60;
const uptimeStr =
hours > 0
? `${String(hours)}h ${String(mins)}m ${String(secs)}s`
: `${String(mins)}m ${String(secs)}s`;
lines.push(`| Uptime | ${uptimeStr} |`);
}
return lines.join("\n");
}
function formatAgents(raw: unknown): string {
let agents: Agent[] = [];
if (Array.isArray(raw)) {
agents = raw as Agent[];
} else if (raw !== null && typeof raw === "object") {
const obj = raw as AgentsResponse;
if (obj.error) {
return `**Agents**\n\nError: ${obj.error}`;
}
if (Array.isArray(obj.agents)) {
agents = obj.agents;
}
}
if (agents.length === 0) {
return "**Agents**\n\nNo agents currently running.";
}
const lines: string[] = [
`**Agents** (${String(agents.length)} total)\n`,
`| ID / Key | Status | Type / Channel | Started |`,
`|---|---|---|---|`,
];
for (const agent of agents) {
const id = agent.id ?? agent.sessionKey ?? "—";
const status = agent.agentStatus ?? agent.status ?? "—";
const type = agent.type ?? agent.channel ?? "—";
const started = agent.startedAt ? new Date(agent.startedAt).toLocaleString() : "—";
lines.push(`| \`${id}\` | ${status} | ${type} | ${started} |`);
}
return lines.join("\n");
}
function formatQueueStats(data: QueueStats): string {
if (data.error) {
return `**Queue Stats**\n\nError: ${data.error}`;
}
const lines: string[] = [`**Queue Statistics**\n`, `| Metric | Count |`, `|---|---|`];
const metrics: [string, number | undefined][] = [
["Pending", data.pending ?? data.waiting],
["Active", data.active],
["Delayed", data.delayed],
["Completed", data.completed],
["Failed", data.failed],
];
for (const [label, value] of metrics) {
if (value !== undefined) {
lines.push(`| ${label} | ${String(value)} |`);
}
}
if (data.paused === true) {
lines.push("\n_Queue is currently **paused**._");
}
return lines.join("\n");
}
function formatAction(command: string, data: ActionResponse): string {
if (data.error) {
return `**${command}** failed.\n\nError: ${data.error}`;
}
const verb = command === "/pause" ? "paused" : "resumed";
const msg = data.message ?? data.status ?? `Queue ${verb} successfully.`;
return `**Queue ${verb}**\n\n${msg}`;
}
function formatHelp(): string {
const lines: string[] = [
"**Available Orchestrator Commands**\n",
"| Command | Description |",
"|---|---|",
];
for (const cmd of ORCHESTRATOR_COMMANDS) {
const name = cmd.aliases ? `${cmd.name} (${cmd.aliases.join(", ")})` : cmd.name;
lines.push(`| \`${name}\` | ${cmd.description} |`);
}
lines.push("\n_Commands starting with `/` are routed to the orchestrator instead of the LLM._");
return lines.join("\n");
}
// ---------------------------------------------------------------------------
// Command parser
// ---------------------------------------------------------------------------
function parseCommandName(content: string): string | null {
const trimmed = content.trim();
if (!trimmed.startsWith("/")) {
return null;
}
const parts = trimmed.split(/\s+/);
return parts[0]?.toLowerCase() ?? null;
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
export interface UseOrchestratorCommandsReturn {
/**
* Returns true if the content looks like an orchestrator command.
*/
isCommand: (content: string) => boolean;
/**
* Execute an orchestrator command.
* Returns a Message with formatted markdown output, or null if not a command.
*/
executeCommand: (content: string) => Promise<Message | null>;
}
export function useOrchestratorCommands(): UseOrchestratorCommandsReturn {
const isCommand = useCallback((content: string): boolean => {
return content.trim().startsWith("/");
}, []);
const executeCommand = useCallback(async (content: string): Promise<Message | null> => {
const command = parseCommandName(content);
if (!command) {
return null;
}
// /help — local, no network
if (command === "/help") {
return makeMessage(formatHelp());
}
// /status
if (command === "/status") {
try {
const res = await fetch("/api/orchestrator/health", { method: "GET" });
const data = (await res.json()) as HealthResponse;
return makeMessage(formatStatus(data));
} catch (err) {
const detail = err instanceof Error ? err.message : "Network error";
return errorMessage("/status", detail);
}
}
// /agents
if (command === "/agents") {
try {
const res = await fetch("/api/orchestrator/agents", { method: "GET" });
const data: unknown = await res.json();
return makeMessage(formatAgents(data));
} catch (err) {
const detail = err instanceof Error ? err.message : "Network error";
return errorMessage("/agents", detail);
}
}
// /jobs or /queue
if (command === "/jobs" || command === "/queue") {
try {
const res = await fetch("/api/orchestrator/queue/stats", { method: "GET" });
const data = (await res.json()) as QueueStats;
return makeMessage(formatQueueStats(data));
} catch (err) {
const detail = err instanceof Error ? err.message : "Network error";
return errorMessage(command, detail);
}
}
// /pause
if (command === "/pause") {
try {
const res = await fetch("/api/orchestrator/queue/pause", { method: "POST" });
const data = (await res.json()) as ActionResponse;
return makeMessage(formatAction("/pause", data));
} catch (err) {
const detail = err instanceof Error ? err.message : "Network error";
return errorMessage("/pause", detail);
}
}
// /resume
if (command === "/resume") {
try {
const res = await fetch("/api/orchestrator/queue/resume", { method: "POST" });
const data = (await res.json()) as ActionResponse;
return makeMessage(formatAction("/resume", data));
} catch (err) {
const detail = err instanceof Error ? err.message : "Network error";
return errorMessage("/resume", detail);
}
}
// Unknown command — show help hint
return makeMessage(
`Unknown command: \`${command}\`\n\nType \`/help\` to see available commands.`
);
}, []);
return { isCommand, executeCommand };
}