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:
Jiayuan 2026-03-26 18:37:56 +08:00
commit 6ee034c6e9
151 changed files with 3664 additions and 6579 deletions

View 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" });
}
}

View file

@ -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";
}
});
}

View 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));
}
}
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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";

View 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;
}

View 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;
}