merge: resolve conflicts after merging main
Adapt runtime features (usage tracking, ping, heartbeat) to main's multi-workspace architecture. Update frontend imports from @multica/types to @/shared/types after the package consolidation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
6ee034c6e9
151 changed files with 3664 additions and 6579 deletions
424
apps/web/shared/api/client.ts
Normal file
424
apps/web/shared/api/client.ts
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
import type {
|
||||
Issue,
|
||||
CreateIssueRequest,
|
||||
UpdateIssueRequest,
|
||||
ListIssuesResponse,
|
||||
UpdateMeRequest,
|
||||
CreateMemberRequest,
|
||||
UpdateMemberRequest,
|
||||
ListIssuesParams,
|
||||
Agent,
|
||||
CreateAgentRequest,
|
||||
UpdateAgentRequest,
|
||||
AgentTask,
|
||||
AgentRuntime,
|
||||
DaemonPairingSession,
|
||||
ApproveDaemonPairingSessionRequest,
|
||||
InboxItem,
|
||||
Comment,
|
||||
Workspace,
|
||||
MemberWithUser,
|
||||
User,
|
||||
Skill,
|
||||
CreateSkillRequest,
|
||||
UpdateSkillRequest,
|
||||
SetAgentSkillsRequest,
|
||||
PersonalAccessToken,
|
||||
CreatePersonalAccessTokenRequest,
|
||||
CreatePersonalAccessTokenResponse,
|
||||
RuntimeUsage,
|
||||
RuntimePing,
|
||||
} from "@/shared/types";
|
||||
import { type Logger, noopLogger } from "@/shared/logger";
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
private token: string | null = null;
|
||||
private workspaceId: string | null = null;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(baseUrl: string, options?: { logger?: Logger }) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.logger = options?.logger ?? noopLogger;
|
||||
}
|
||||
|
||||
setToken(token: string | null) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
setWorkspaceId(id: string | null) {
|
||||
this.workspaceId = id;
|
||||
}
|
||||
|
||||
private async fetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const rid = crypto.randomUUID().slice(0, 8);
|
||||
const start = Date.now();
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Request-ID": rid,
|
||||
...((init?.headers as Record<string, string>) ?? {}),
|
||||
};
|
||||
if (this.token) {
|
||||
headers["Authorization"] = `Bearer ${this.token}`;
|
||||
}
|
||||
if (this.workspaceId) {
|
||||
headers["X-Workspace-ID"] = this.workspaceId;
|
||||
}
|
||||
|
||||
this.logger.info(`→ ${method} ${path}`, { rid });
|
||||
|
||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401 && typeof window !== "undefined") {
|
||||
localStorage.removeItem("multica_token");
|
||||
localStorage.removeItem("multica_workspace_id");
|
||||
this.token = null;
|
||||
this.workspaceId = null;
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
|
||||
let message = `API error: ${res.status} ${res.statusText}`;
|
||||
try {
|
||||
const data = await res.json() as { error?: string };
|
||||
if (typeof data.error === "string" && data.error) {
|
||||
message = data.error;
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON error bodies.
|
||||
}
|
||||
this.logger.error(`← ${res.status} ${path}`, { rid, duration: `${Date.now() - start}ms`, error: message });
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
this.logger.info(`← ${res.status} ${path}`, { rid, duration: `${Date.now() - start}ms` });
|
||||
|
||||
// Handle 204 No Content
|
||||
if (res.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
// Auth
|
||||
async sendCode(email: string): Promise<void> {
|
||||
await this.fetch("/auth/send-code", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
}
|
||||
|
||||
async verifyCode(email: string, code: string): Promise<LoginResponse> {
|
||||
return this.fetch("/auth/verify-code", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, code }),
|
||||
});
|
||||
}
|
||||
|
||||
async getMe(): Promise<User> {
|
||||
return this.fetch("/api/me");
|
||||
}
|
||||
|
||||
async updateMe(data: UpdateMeRequest): Promise<User> {
|
||||
return this.fetch("/api/me", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// Issues
|
||||
async listIssues(params?: ListIssuesParams): Promise<ListIssuesResponse> {
|
||||
const search = new URLSearchParams();
|
||||
if (params?.limit) search.set("limit", String(params.limit));
|
||||
if (params?.offset) search.set("offset", String(params.offset));
|
||||
const wsId = params?.workspace_id ?? this.workspaceId;
|
||||
if (wsId) search.set("workspace_id", wsId);
|
||||
if (params?.status) search.set("status", params.status);
|
||||
if (params?.priority) search.set("priority", params.priority);
|
||||
if (params?.assignee_id) search.set("assignee_id", params.assignee_id);
|
||||
return this.fetch(`/api/issues?${search}`);
|
||||
}
|
||||
|
||||
async getIssue(id: string): Promise<Issue> {
|
||||
return this.fetch(`/api/issues/${id}`);
|
||||
}
|
||||
|
||||
async createIssue(data: CreateIssueRequest): Promise<Issue> {
|
||||
const search = new URLSearchParams();
|
||||
if (this.workspaceId) search.set("workspace_id", this.workspaceId);
|
||||
return this.fetch(`/api/issues?${search}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async updateIssue(id: string, data: UpdateIssueRequest): Promise<Issue> {
|
||||
return this.fetch(`/api/issues/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssue(id: string): Promise<void> {
|
||||
await this.fetch(`/api/issues/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
// Comments
|
||||
async listComments(issueId: string): Promise<Comment[]> {
|
||||
return this.fetch(`/api/issues/${issueId}/comments`);
|
||||
}
|
||||
|
||||
async createComment(issueId: string, content: string, type?: string): Promise<Comment> {
|
||||
return this.fetch(`/api/issues/${issueId}/comments`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content, type: type ?? "comment" }),
|
||||
});
|
||||
}
|
||||
|
||||
async updateComment(commentId: string, content: string): Promise<Comment> {
|
||||
return this.fetch(`/api/comments/${commentId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteComment(commentId: string): Promise<void> {
|
||||
await this.fetch(`/api/comments/${commentId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
// Agents
|
||||
async listAgents(params?: { workspace_id?: string }): Promise<Agent[]> {
|
||||
const search = new URLSearchParams();
|
||||
const wsId = params?.workspace_id ?? this.workspaceId;
|
||||
if (wsId) search.set("workspace_id", wsId);
|
||||
return this.fetch(`/api/agents?${search}`);
|
||||
}
|
||||
|
||||
async getAgent(id: string): Promise<Agent> {
|
||||
return this.fetch(`/api/agents/${id}`);
|
||||
}
|
||||
|
||||
async createAgent(data: CreateAgentRequest): Promise<Agent> {
|
||||
return this.fetch("/api/agents", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async updateAgent(id: string, data: UpdateAgentRequest): Promise<Agent> {
|
||||
return this.fetch(`/api/agents/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAgent(id: string): Promise<void> {
|
||||
await this.fetch(`/api/agents/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
async listRuntimes(params?: { workspace_id?: string }): Promise<AgentRuntime[]> {
|
||||
const search = new URLSearchParams();
|
||||
const wsId = params?.workspace_id ?? this.workspaceId;
|
||||
if (wsId) search.set("workspace_id", wsId);
|
||||
return this.fetch(`/api/runtimes?${search}`);
|
||||
}
|
||||
|
||||
async getRuntimeUsage(runtimeId: string, params?: { days?: number }): Promise<RuntimeUsage[]> {
|
||||
const search = new URLSearchParams();
|
||||
if (params?.days) search.set("days", String(params.days));
|
||||
return this.fetch(`/api/runtimes/${runtimeId}/usage?${search}`);
|
||||
}
|
||||
|
||||
async pingRuntime(runtimeId: string): Promise<RuntimePing> {
|
||||
return this.fetch(`/api/runtimes/${runtimeId}/ping`, { method: "POST" });
|
||||
}
|
||||
|
||||
async getPingResult(runtimeId: string, pingId: string): Promise<RuntimePing> {
|
||||
return this.fetch(`/api/runtimes/${runtimeId}/ping/${pingId}`);
|
||||
}
|
||||
|
||||
async listAgentTasks(agentId: string): Promise<AgentTask[]> {
|
||||
return this.fetch(`/api/agents/${agentId}/tasks`);
|
||||
}
|
||||
|
||||
async getDaemonPairingSession(token: string): Promise<DaemonPairingSession> {
|
||||
return this.fetch(`/api/daemon/pairing-sessions/${token}`);
|
||||
}
|
||||
|
||||
async approveDaemonPairingSession(
|
||||
token: string,
|
||||
data: ApproveDaemonPairingSessionRequest,
|
||||
): Promise<DaemonPairingSession> {
|
||||
return this.fetch(`/api/daemon/pairing-sessions/${token}/approve`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// Inbox
|
||||
async listInbox(): Promise<InboxItem[]> {
|
||||
return this.fetch("/api/inbox");
|
||||
}
|
||||
|
||||
async markInboxRead(id: string): Promise<InboxItem> {
|
||||
return this.fetch(`/api/inbox/${id}/read`, { method: "POST" });
|
||||
}
|
||||
|
||||
async archiveInbox(id: string): Promise<InboxItem> {
|
||||
return this.fetch(`/api/inbox/${id}/archive`, { method: "POST" });
|
||||
}
|
||||
|
||||
async getUnreadInboxCount(): Promise<{ count: number }> {
|
||||
return this.fetch("/api/inbox/unread-count");
|
||||
}
|
||||
|
||||
async markAllInboxRead(): Promise<{ count: number }> {
|
||||
return this.fetch("/api/inbox/mark-all-read", { method: "POST" });
|
||||
}
|
||||
|
||||
async archiveAllInbox(): Promise<{ count: number }> {
|
||||
return this.fetch("/api/inbox/archive-all", { method: "POST" });
|
||||
}
|
||||
|
||||
async archiveAllReadInbox(): Promise<{ count: number }> {
|
||||
return this.fetch("/api/inbox/archive-all-read", { method: "POST" });
|
||||
}
|
||||
|
||||
async archiveCompletedInbox(): Promise<{ count: number }> {
|
||||
return this.fetch("/api/inbox/archive-completed", { method: "POST" });
|
||||
}
|
||||
|
||||
// Workspaces
|
||||
async listWorkspaces(): Promise<Workspace[]> {
|
||||
return this.fetch("/api/workspaces");
|
||||
}
|
||||
|
||||
async getWorkspace(id: string): Promise<Workspace> {
|
||||
return this.fetch(`/api/workspaces/${id}`);
|
||||
}
|
||||
|
||||
async createWorkspace(data: { name: string; slug: string; description?: string; context?: string }): Promise<Workspace> {
|
||||
return this.fetch("/api/workspaces", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkspace(id: string, data: { name?: string; description?: string; context?: string; settings?: Record<string, unknown> }): Promise<Workspace> {
|
||||
return this.fetch(`/api/workspaces/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// Members
|
||||
async listMembers(workspaceId: string): Promise<MemberWithUser[]> {
|
||||
return this.fetch(`/api/workspaces/${workspaceId}/members`);
|
||||
}
|
||||
|
||||
async createMember(workspaceId: string, data: CreateMemberRequest): Promise<MemberWithUser> {
|
||||
return this.fetch(`/api/workspaces/${workspaceId}/members`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async updateMember(workspaceId: string, memberId: string, data: UpdateMemberRequest): Promise<MemberWithUser> {
|
||||
return this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteMember(workspaceId: string, memberId: string): Promise<void> {
|
||||
await this.fetch(`/api/workspaces/${workspaceId}/members/${memberId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
async leaveWorkspace(workspaceId: string): Promise<void> {
|
||||
await this.fetch(`/api/workspaces/${workspaceId}/leave`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
async deleteWorkspace(workspaceId: string): Promise<void> {
|
||||
await this.fetch(`/api/workspaces/${workspaceId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
// Skills
|
||||
async listSkills(): Promise<Skill[]> {
|
||||
return this.fetch("/api/skills");
|
||||
}
|
||||
|
||||
async getSkill(id: string): Promise<Skill> {
|
||||
return this.fetch(`/api/skills/${id}`);
|
||||
}
|
||||
|
||||
async createSkill(data: CreateSkillRequest): Promise<Skill> {
|
||||
return this.fetch("/api/skills", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async updateSkill(id: string, data: UpdateSkillRequest): Promise<Skill> {
|
||||
return this.fetch(`/api/skills/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSkill(id: string): Promise<void> {
|
||||
await this.fetch(`/api/skills/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
async importSkill(data: { url: string }): Promise<Skill> {
|
||||
return this.fetch("/api/skills/import", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async listAgentSkills(agentId: string): Promise<Skill[]> {
|
||||
return this.fetch(`/api/agents/${agentId}/skills`);
|
||||
}
|
||||
|
||||
async setAgentSkills(agentId: string, data: SetAgentSkillsRequest): Promise<void> {
|
||||
await this.fetch(`/api/agents/${agentId}/skills`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// Personal Access Tokens
|
||||
async listPersonalAccessTokens(): Promise<PersonalAccessToken[]> {
|
||||
return this.fetch("/api/tokens");
|
||||
}
|
||||
|
||||
async createPersonalAccessToken(data: CreatePersonalAccessTokenRequest): Promise<CreatePersonalAccessTokenResponse> {
|
||||
return this.fetch("/api/tokens", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async revokePersonalAccessToken(id: string): Promise<void> {
|
||||
await this.fetch(`/api/tokens/${id}`, { method: "DELETE" });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import { ApiClient } from "@multica/sdk";
|
||||
import { createLogger } from "./logger";
|
||||
import { createLogger } from "@/shared/logger";
|
||||
import { ApiClient } from "./client";
|
||||
|
||||
export { ApiClient } from "./client";
|
||||
export type { LoginResponse } from "./client";
|
||||
export { WSClient } from "./ws-client";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080";
|
||||
|
||||
|
|
@ -16,13 +20,4 @@ if (typeof window !== "undefined") {
|
|||
api.setWorkspaceId(wsId);
|
||||
}
|
||||
|
||||
api.setOnUnauthorized(() => {
|
||||
localStorage.removeItem("multica_token");
|
||||
localStorage.removeItem("multica_workspace_id");
|
||||
api.setToken(null);
|
||||
api.setWorkspaceId(null);
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
});
|
||||
}
|
||||
110
apps/web/shared/api/ws-client.ts
Normal file
110
apps/web/shared/api/ws-client.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import type { WSMessage, WSEventType } from "@/shared/types";
|
||||
import { type Logger, noopLogger } from "@/shared/logger";
|
||||
|
||||
type EventHandler = (payload: unknown) => void;
|
||||
|
||||
export class WSClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private baseUrl: string;
|
||||
private token: string | null = null;
|
||||
private workspaceId: string | null = null;
|
||||
private handlers = new Map<WSEventType, Set<EventHandler>>();
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private hasConnectedBefore = false;
|
||||
private onReconnectCallbacks = new Set<() => void>();
|
||||
private logger: Logger;
|
||||
|
||||
constructor(url: string, options?: { logger?: Logger }) {
|
||||
this.baseUrl = url;
|
||||
this.logger = options?.logger ?? noopLogger;
|
||||
}
|
||||
|
||||
setAuth(token: string, workspaceId: string) {
|
||||
this.token = token;
|
||||
this.workspaceId = workspaceId;
|
||||
}
|
||||
|
||||
connect() {
|
||||
const url = new URL(this.baseUrl);
|
||||
if (this.token) url.searchParams.set("token", this.token);
|
||||
if (this.workspaceId)
|
||||
url.searchParams.set("workspace_id", this.workspaceId);
|
||||
|
||||
this.ws = new WebSocket(url.toString());
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.logger.info("connected");
|
||||
if (this.hasConnectedBefore) {
|
||||
for (const cb of this.onReconnectCallbacks) {
|
||||
try {
|
||||
cb();
|
||||
} catch {
|
||||
// ignore reconnect callback errors
|
||||
}
|
||||
}
|
||||
}
|
||||
this.hasConnectedBefore = true;
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data as string) as WSMessage;
|
||||
this.logger.debug("received", msg.type);
|
||||
const eventHandlers = this.handlers.get(msg.type);
|
||||
if (eventHandlers) {
|
||||
for (const handler of eventHandlers) {
|
||||
handler(msg.payload);
|
||||
}
|
||||
} else {
|
||||
this.logger.debug("unhandled event", msg.type);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.logger.warn("disconnected, reconnecting in 3s");
|
||||
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
|
||||
};
|
||||
|
||||
this.ws.onerror = () => {
|
||||
// Suppress — onclose handles reconnect; errors during StrictMode
|
||||
// double-fire are expected in dev and harmless.
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.ws) {
|
||||
// Remove handlers before close to prevent onclose from scheduling a reconnect
|
||||
this.ws.onclose = null;
|
||||
this.ws.onerror = null;
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.hasConnectedBefore = false;
|
||||
}
|
||||
|
||||
on(event: WSEventType, handler: EventHandler) {
|
||||
if (!this.handlers.has(event)) {
|
||||
this.handlers.set(event, new Set());
|
||||
}
|
||||
this.handlers.get(event)!.add(handler);
|
||||
return () => {
|
||||
this.handlers.get(event)?.delete(handler);
|
||||
};
|
||||
}
|
||||
|
||||
onReconnect(callback: () => void) {
|
||||
this.onReconnectCallbacks.add(callback);
|
||||
return () => {
|
||||
this.onReconnectCallbacks.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
send(message: WSMessage) {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
168
apps/web/shared/types/agent.ts
Normal file
168
apps/web/shared/types/agent.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
export type AgentStatus = "idle" | "working" | "blocked" | "error" | "offline";
|
||||
|
||||
export type AgentRuntimeMode = "local" | "cloud";
|
||||
|
||||
export type AgentVisibility = "workspace" | "private";
|
||||
|
||||
export type AgentTriggerType = "on_assign" | "scheduled";
|
||||
|
||||
export interface RuntimeDevice {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
daemon_id: string | null;
|
||||
name: string;
|
||||
runtime_mode: AgentRuntimeMode;
|
||||
provider: string;
|
||||
status: "online" | "offline";
|
||||
device_info: string;
|
||||
metadata: Record<string, unknown>;
|
||||
last_seen_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type AgentRuntime = RuntimeDevice;
|
||||
|
||||
export interface AgentTool {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
auth_type: "oauth" | "api_key" | "none";
|
||||
connected: boolean;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentTrigger {
|
||||
id: string;
|
||||
type: AgentTriggerType;
|
||||
enabled: boolean;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentTask {
|
||||
id: string;
|
||||
agent_id: string;
|
||||
runtime_id: string;
|
||||
issue_id: string;
|
||||
status: "queued" | "dispatched" | "running" | "completed" | "failed" | "cancelled";
|
||||
priority: number;
|
||||
dispatched_at: string | null;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
result: unknown;
|
||||
error: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
runtime_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
avatar_url: string | null;
|
||||
runtime_mode: AgentRuntimeMode;
|
||||
runtime_config: Record<string, unknown>;
|
||||
visibility: AgentVisibility;
|
||||
status: AgentStatus;
|
||||
max_concurrent_tasks: number;
|
||||
owner_id: string | null;
|
||||
skills: Skill[];
|
||||
tools: AgentTool[];
|
||||
triggers: AgentTrigger[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateAgentRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
avatar_url?: string;
|
||||
runtime_id: string;
|
||||
runtime_config?: Record<string, unknown>;
|
||||
visibility?: AgentVisibility;
|
||||
max_concurrent_tasks?: number;
|
||||
tools?: AgentTool[];
|
||||
triggers?: AgentTrigger[];
|
||||
}
|
||||
|
||||
export interface UpdateAgentRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
avatar_url?: string;
|
||||
runtime_id?: string;
|
||||
runtime_config?: Record<string, unknown>;
|
||||
visibility?: AgentVisibility;
|
||||
status?: AgentStatus;
|
||||
max_concurrent_tasks?: number;
|
||||
tools?: AgentTool[];
|
||||
triggers?: AgentTrigger[];
|
||||
}
|
||||
|
||||
// Skills
|
||||
|
||||
export interface Skill {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
config: Record<string, unknown>;
|
||||
files: SkillFile[];
|
||||
created_by: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SkillFile {
|
||||
id: string;
|
||||
skill_id: string;
|
||||
path: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateSkillRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
config?: Record<string, unknown>;
|
||||
files?: { path: string; content: string }[];
|
||||
}
|
||||
|
||||
export interface UpdateSkillRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
config?: Record<string, unknown>;
|
||||
files?: { path: string; content: string }[];
|
||||
}
|
||||
|
||||
export interface SetAgentSkillsRequest {
|
||||
skill_ids: string[];
|
||||
}
|
||||
|
||||
export type RuntimePingStatus = "pending" | "running" | "completed" | "failed" | "timeout";
|
||||
|
||||
export interface RuntimePing {
|
||||
id: string;
|
||||
runtime_id: string;
|
||||
status: RuntimePingStatus;
|
||||
output?: string;
|
||||
error?: string;
|
||||
duration_ms?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface RuntimeUsage {
|
||||
runtime_id: string;
|
||||
date: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_read_tokens: number;
|
||||
cache_write_tokens: number;
|
||||
}
|
||||
82
apps/web/shared/types/api.ts
Normal file
82
apps/web/shared/types/api.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import type { Issue, IssueStatus, IssuePriority, IssueAssigneeType } from "./issue";
|
||||
import type { MemberRole } from "./workspace";
|
||||
|
||||
// Issue API
|
||||
export interface CreateIssueRequest {
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: IssueStatus;
|
||||
priority?: IssuePriority;
|
||||
assignee_type?: IssueAssigneeType;
|
||||
assignee_id?: string;
|
||||
parent_issue_id?: string;
|
||||
acceptance_criteria?: string[];
|
||||
context_refs?: string[];
|
||||
due_date?: string;
|
||||
}
|
||||
|
||||
export interface UpdateIssueRequest {
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: IssueStatus;
|
||||
priority?: IssuePriority;
|
||||
assignee_type?: IssueAssigneeType | null;
|
||||
assignee_id?: string | null;
|
||||
position?: number;
|
||||
due_date?: string | null;
|
||||
acceptance_criteria?: string[];
|
||||
context_refs?: string[];
|
||||
}
|
||||
|
||||
export interface ListIssuesParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
workspace_id?: string;
|
||||
status?: IssueStatus;
|
||||
priority?: IssuePriority;
|
||||
assignee_id?: string;
|
||||
}
|
||||
|
||||
export interface ListIssuesResponse {
|
||||
issues: Issue[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UpdateMeRequest {
|
||||
name?: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
export interface CreateMemberRequest {
|
||||
email: string;
|
||||
role?: MemberRole;
|
||||
}
|
||||
|
||||
export interface UpdateMemberRequest {
|
||||
role: MemberRole;
|
||||
}
|
||||
|
||||
// Personal Access Tokens
|
||||
export interface PersonalAccessToken {
|
||||
id: string;
|
||||
name: string;
|
||||
token_prefix: string;
|
||||
expires_at: string | null;
|
||||
last_used_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreatePersonalAccessTokenRequest {
|
||||
name: string;
|
||||
expires_in_days?: number;
|
||||
}
|
||||
|
||||
export interface CreatePersonalAccessTokenResponse extends PersonalAccessToken {
|
||||
token: string;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
export interface PaginationParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
14
apps/web/shared/types/comment.ts
Normal file
14
apps/web/shared/types/comment.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type CommentType = "comment" | "status_change" | "progress_update" | "system";
|
||||
|
||||
export type CommentAuthorType = "member" | "agent";
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
issue_id: string;
|
||||
author_type: CommentAuthorType;
|
||||
author_id: string;
|
||||
content: string;
|
||||
type: CommentType;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
22
apps/web/shared/types/daemon.ts
Normal file
22
apps/web/shared/types/daemon.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export type DaemonPairingSessionStatus = "pending" | "approved" | "claimed" | "expired";
|
||||
|
||||
export interface DaemonPairingSession {
|
||||
token: string;
|
||||
daemon_id: string;
|
||||
device_name: string;
|
||||
runtime_name: string;
|
||||
runtime_type: string;
|
||||
runtime_version: string;
|
||||
workspace_id: string | null;
|
||||
status: DaemonPairingSessionStatus;
|
||||
approved_at: string | null;
|
||||
claimed_at: string | null;
|
||||
expires_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
link_url?: string | null;
|
||||
}
|
||||
|
||||
export interface ApproveDaemonPairingSessionRequest {
|
||||
workspace_id: string;
|
||||
}
|
||||
126
apps/web/shared/types/events.ts
Normal file
126
apps/web/shared/types/events.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import type { Issue } from "./issue";
|
||||
import type { Agent } from "./agent";
|
||||
import type { InboxItem } from "./inbox";
|
||||
import type { Comment } from "./comment";
|
||||
import type { Workspace, MemberWithUser } from "./workspace";
|
||||
|
||||
// WebSocket event types (matching Go server protocol/events.go)
|
||||
export type WSEventType =
|
||||
| "issue:created"
|
||||
| "issue:updated"
|
||||
| "issue:deleted"
|
||||
| "comment:created"
|
||||
| "comment:updated"
|
||||
| "comment:deleted"
|
||||
| "agent:status"
|
||||
| "agent:created"
|
||||
| "agent:deleted"
|
||||
| "task:dispatch"
|
||||
| "task:progress"
|
||||
| "task:completed"
|
||||
| "task:failed"
|
||||
| "inbox:new"
|
||||
| "inbox:read"
|
||||
| "inbox:archived"
|
||||
| "inbox:batch-read"
|
||||
| "inbox:batch-archived"
|
||||
| "workspace:updated"
|
||||
| "workspace:deleted"
|
||||
| "member:added"
|
||||
| "member:updated"
|
||||
| "member:removed"
|
||||
| "daemon:heartbeat"
|
||||
| "daemon:register"
|
||||
| "skill:created"
|
||||
| "skill:updated"
|
||||
| "skill:deleted";
|
||||
|
||||
export interface WSMessage<T = unknown> {
|
||||
type: WSEventType;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export interface IssueCreatedPayload {
|
||||
issue: Issue;
|
||||
}
|
||||
|
||||
export interface IssueUpdatedPayload {
|
||||
issue: Issue;
|
||||
}
|
||||
|
||||
export interface IssueDeletedPayload {
|
||||
issue_id: string;
|
||||
}
|
||||
|
||||
export interface AgentStatusPayload {
|
||||
agent: Agent;
|
||||
}
|
||||
|
||||
export interface AgentCreatedPayload {
|
||||
agent: Agent;
|
||||
}
|
||||
|
||||
export interface AgentDeletedPayload {
|
||||
agent_id: string;
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
export interface InboxNewPayload {
|
||||
item: InboxItem;
|
||||
}
|
||||
|
||||
export interface InboxReadPayload {
|
||||
item_id: string;
|
||||
recipient_id: string;
|
||||
}
|
||||
|
||||
export interface InboxArchivedPayload {
|
||||
item_id: string;
|
||||
recipient_id: string;
|
||||
}
|
||||
|
||||
export interface InboxBatchReadPayload {
|
||||
recipient_id: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface InboxBatchArchivedPayload {
|
||||
recipient_id: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface CommentCreatedPayload {
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
export interface CommentUpdatedPayload {
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
export interface CommentDeletedPayload {
|
||||
comment_id: string;
|
||||
issue_id: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceUpdatedPayload {
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export interface WorkspaceDeletedPayload {
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
export interface MemberUpdatedPayload {
|
||||
member: MemberWithUser;
|
||||
}
|
||||
|
||||
export interface MemberAddedPayload {
|
||||
member: MemberWithUser;
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
export interface MemberRemovedPayload {
|
||||
member_id: string;
|
||||
user_id: string;
|
||||
workspace_id: string;
|
||||
}
|
||||
29
apps/web/shared/types/inbox.ts
Normal file
29
apps/web/shared/types/inbox.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { IssueStatus } from "./issue";
|
||||
|
||||
export type InboxSeverity = "action_required" | "attention" | "info";
|
||||
|
||||
export type InboxItemType =
|
||||
| "issue_assigned"
|
||||
| "review_requested"
|
||||
| "agent_blocked"
|
||||
| "agent_completed"
|
||||
| "mentioned"
|
||||
| "status_change";
|
||||
|
||||
export interface InboxItem {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
recipient_type: "member" | "agent";
|
||||
recipient_id: string;
|
||||
actor_type: "member" | "agent" | null;
|
||||
actor_id: string | null;
|
||||
type: InboxItemType;
|
||||
severity: InboxSeverity;
|
||||
issue_id: string | null;
|
||||
title: string;
|
||||
body: string | null;
|
||||
issue_status: IssueStatus | null;
|
||||
read: boolean;
|
||||
archived: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
29
apps/web/shared/types/index.ts
Normal file
29
apps/web/shared/types/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export type { Issue, IssueStatus, IssuePriority, IssueAssigneeType } from "./issue";
|
||||
export type {
|
||||
Agent,
|
||||
AgentStatus,
|
||||
AgentRuntimeMode,
|
||||
AgentVisibility,
|
||||
AgentTriggerType,
|
||||
AgentTool,
|
||||
AgentTrigger,
|
||||
AgentTask,
|
||||
AgentRuntime,
|
||||
RuntimeDevice,
|
||||
CreateAgentRequest,
|
||||
UpdateAgentRequest,
|
||||
Skill,
|
||||
SkillFile,
|
||||
CreateSkillRequest,
|
||||
UpdateSkillRequest,
|
||||
SetAgentSkillsRequest,
|
||||
RuntimeUsage,
|
||||
RuntimePing,
|
||||
RuntimePingStatus,
|
||||
} from "./agent";
|
||||
export type { Workspace, Member, MemberRole, User, MemberWithUser } from "./workspace";
|
||||
export type { InboxItem, InboxSeverity, InboxItemType } from "./inbox";
|
||||
export type { Comment, CommentType, CommentAuthorType } from "./comment";
|
||||
export type { DaemonPairingSession, DaemonPairingSessionStatus, ApproveDaemonPairingSessionRequest } from "./daemon";
|
||||
export type * from "./events";
|
||||
export type * from "./api";
|
||||
32
apps/web/shared/types/issue.ts
Normal file
32
apps/web/shared/types/issue.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export type IssueStatus =
|
||||
| "backlog"
|
||||
| "todo"
|
||||
| "in_progress"
|
||||
| "in_review"
|
||||
| "done"
|
||||
| "blocked"
|
||||
| "cancelled";
|
||||
|
||||
export type IssuePriority = "urgent" | "high" | "medium" | "low" | "none";
|
||||
|
||||
export type IssueAssigneeType = "member" | "agent";
|
||||
|
||||
export interface Issue {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: IssueStatus;
|
||||
priority: IssuePriority;
|
||||
assignee_type: IssueAssigneeType | null;
|
||||
assignee_id: string | null;
|
||||
creator_type: IssueAssigneeType;
|
||||
creator_id: string;
|
||||
parent_issue_id: string | null;
|
||||
acceptance_criteria: string[];
|
||||
context_refs: string[];
|
||||
position: number;
|
||||
due_date: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
40
apps/web/shared/types/workspace.ts
Normal file
40
apps/web/shared/types/workspace.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
export type MemberRole = "owner" | "admin" | "member";
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
context: string | null;
|
||||
settings: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
user_id: string;
|
||||
role: MemberRole;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MemberWithUser {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
user_id: string;
|
||||
role: MemberRole;
|
||||
created_at: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string | null;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue