fix: open ai workspace console in ops

This commit is contained in:
DCCONSTRUCTIONS
2026-06-09 14:35:34 +03:00
parent 342df60c24
commit b1d19cf5df
4 changed files with 2026 additions and 137 deletions
@@ -4,23 +4,13 @@
* See the LICENSE file for details.
*/
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import useSWR from "swr";
import { MessageCircle } from "lucide-react";
// plane imports
import { Tooltip } from "@plane/propel/tooltip";
// components
import { AIWorkspaceProductSettingsModal } from "@/components/ai-workspace/product-settings-modal";
// services
import { WorkspaceAIWorkspaceService } from "@/services/workspace-ai-workspace.service";
import type {
TAIWorkspaceExecutor,
TAIWorkspaceExecutorInput,
TAIWorkspaceExecutorListResponse,
} from "@/services/workspace-ai-workspace.service";
const workspaceAIWorkspaceService = new WorkspaceAIWorkspaceService();
import { AIWorkspaceProductConsole } from "@/components/ai-workspace/product-console";
type TAIWorkspaceGlobalControlProps = {
workspaceSlug: string;
@@ -29,15 +19,6 @@ type TAIWorkspaceGlobalControlProps = {
export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) {
const [isOpen, setIsOpen] = useState(false);
const [dockSlot, setDockSlot] = useState<Element | null>(null);
const [busyId, setBusyId] = useState("");
const [notice, setNotice] = useState("");
const [actionError, setActionError] = useState("");
const { data, error, isLoading, mutate } = useSWR(
isOpen && workspaceSlug ? `AI_WORKSPACE_EXECUTORS_${workspaceSlug}` : null,
() => workspaceAIWorkspaceService.listExecutors(workspaceSlug),
{ refreshInterval: isOpen ? 10000 : 0 }
);
useEffect(() => {
if (typeof document === "undefined") return;
@@ -58,98 +39,6 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
};
}, []);
const handleClose = useCallback(() => {
setIsOpen(false);
setNotice("");
setActionError("");
}, []);
const runMutation = useCallback(
async <T,>(id: string, action: () => Promise<T>, successText: string): Promise<T | null> => {
setBusyId(id);
setActionError("");
setNotice("");
try {
const result = await action();
setNotice(successText);
return result;
} catch (mutationError: any) {
setActionError(
String(mutationError?.message || mutationError?.error || mutationError || "AI Workspace request failed")
);
return null;
} finally {
setBusyId("");
}
},
[]
);
const updateRegistry = useCallback(
async (payload: TAIWorkspaceExecutorListResponse) => {
await mutate(payload, { revalidate: false });
return payload;
},
[mutate]
);
const selectExecutor = useCallback(
async (executor: TAIWorkspaceExecutor) =>
runMutation(
`select:${executor.id}`,
async () => updateRegistry(await workspaceAIWorkspaceService.selectExecutor(workspaceSlug, executor.id)),
"Устройство выбрано"
),
[runMutation, updateRegistry, workspaceSlug]
);
const createExecutor = useCallback(
async (input: TAIWorkspaceExecutorInput) =>
runMutation(
"save",
async () => updateRegistry(await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input)),
"Устройство создано"
),
[runMutation, updateRegistry, workspaceSlug]
);
const updateExecutor = useCallback(
async (executorId: string, input: TAIWorkspaceExecutorInput) =>
runMutation(
"save",
async () => updateRegistry(await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, executorId, input)),
"Устройство сохранено"
),
[runMutation, updateRegistry, workspaceSlug]
);
const deleteExecutor = useCallback(
async (executorId: string) =>
runMutation(
`delete:${executorId}`,
async () => updateRegistry(await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, executorId)),
"Устройство удалено"
),
[runMutation, updateRegistry, workspaceSlug]
);
const downloadAgent = useCallback(
async (executorId: string, input: TAIWorkspaceExecutorInput, port: string) => {
await runMutation(
"download",
async () => {
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, executorId, input);
await updateRegistry(payload);
window.location.href = workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, executorId, {
port,
});
},
"Installer подготовлен"
);
},
[runMutation, updateRegistry, workspaceSlug]
);
return (
<>
{dockSlot
@@ -168,26 +57,7 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
)
: null}
<AIWorkspaceProductSettingsModal
isOpen={isOpen}
executors={data?.executors ?? []}
selectedExecutorId={data?.selectedExecutorId ?? null}
isLoading={isLoading}
error={error}
notice={notice}
actionError={actionError}
busyId={busyId}
onClose={handleClose}
onReload={() => mutate()}
onSelect={selectExecutor}
onCreate={createExecutor}
onUpdate={updateExecutor}
onDelete={deleteExecutor}
onDownloadAgent={downloadAgent}
getWindowsAgentInstallerUrl={(executorId, options) =>
workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, executorId, options)
}
/>
<AIWorkspaceProductConsole open={isOpen} workspaceSlug={workspaceSlug} onClose={() => setIsOpen(false)} />
</>
);
}
File diff suppressed because it is too large Load Diff
@@ -50,6 +50,64 @@ export type TAIWorkspaceExecutorInput = {
status?: TAIWorkspaceExecutorStatus;
};
export type TAIWorkspaceThread = {
id: string;
title: string;
originSurface: "engine" | "ops" | "global" | string;
activeContext?: Record<string, any>;
enabledToolPacks?: string[];
linkedArtifacts?: Record<string, any>[];
selectedExecutorId?: string | null;
lifecycleState?: "active" | "archived" | string;
metadata?: Record<string, any>;
createdAt?: string | null;
updatedAt?: string | null;
};
export type TAIWorkspaceThreadMessageRole = "system" | "user" | "assistant" | "tool";
export type TAIWorkspaceThreadMessage = {
id: string;
threadId: string;
sequence?: number;
role: TAIWorkspaceThreadMessageRole;
content: string;
payload?: Record<string, any>;
createdAt?: string | null;
};
export type TAIWorkspaceThreadListResponse = {
ok: boolean;
threads: TAIWorkspaceThread[];
};
export type TAIWorkspaceThreadResponse = {
ok: boolean;
thread: TAIWorkspaceThread;
};
export type TAIWorkspaceThreadMessagesResponse = {
ok: boolean;
threadId: string;
messages: TAIWorkspaceThreadMessage[];
};
export type TAIWorkspaceThreadMessageResponse = {
ok: boolean;
message: TAIWorkspaceThreadMessage;
};
export type TAIWorkspaceThreadInput = {
title: string;
originSurface?: "engine" | "ops" | "global";
activeContext?: Record<string, any>;
enabledToolPacks?: string[];
linkedArtifacts?: Record<string, any>[];
selectedExecutorId?: string | null;
lifecycleState?: "active" | "archived";
metadata?: Record<string, any>;
};
export class WorkspaceAIWorkspaceService extends APIService {
constructor() {
super(API_BASE_URL);
@@ -63,10 +121,15 @@ export class WorkspaceAIWorkspaceService extends APIService {
});
}
async createExecutor(workspaceSlug: string, input: TAIWorkspaceExecutorInput): Promise<TAIWorkspaceExecutorListResponse> {
await this.post(`/api/workspaces/${workspaceSlug}/ai-workspace/executors/`, { ...input, select: true }).catch((error) => {
throw error?.response?.data ?? error;
});
async createExecutor(
workspaceSlug: string,
input: TAIWorkspaceExecutorInput
): Promise<TAIWorkspaceExecutorListResponse> {
await this.post(`/api/workspaces/${workspaceSlug}/ai-workspace/executors/`, { ...input, select: true }).catch(
(error) => {
throw error?.response?.data ?? error;
}
);
return this.listExecutors(workspaceSlug);
}
@@ -101,4 +164,68 @@ export class WorkspaceAIWorkspaceService extends APIService {
if (port) params.set("port", port);
return `${API_BASE_URL}/api/workspaces/${workspaceSlug}/ai-workspace/executors/${executorId}/agent/windows.ps1?${params.toString()}`;
}
async listThreads(
workspaceSlug: string,
options: { surface?: "ops" | "engine" | "global"; limit?: number } = {}
): Promise<TAIWorkspaceThreadListResponse> {
const params = new URLSearchParams();
params.set("surface", options.surface || "ops");
params.set("limit", String(options.limit || 100));
return this.get(`/api/workspaces/${workspaceSlug}/ai-workspace/threads/?${params.toString()}`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data ?? error;
});
}
async createThread(workspaceSlug: string, input: TAIWorkspaceThreadInput): Promise<TAIWorkspaceThreadResponse> {
return this.post(`/api/workspaces/${workspaceSlug}/ai-workspace/threads/`, {
originSurface: "ops",
enabledToolPacks: ["ops"],
...input,
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data ?? error;
});
}
async updateThread(
workspaceSlug: string,
threadId: string,
input: Partial<TAIWorkspaceThreadInput>
): Promise<TAIWorkspaceThreadResponse> {
return this.patch(`/api/workspaces/${workspaceSlug}/ai-workspace/threads/${threadId}/`, input)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data ?? error;
});
}
async listThreadMessages(
workspaceSlug: string,
threadId: string,
options: { limit?: number } = {}
): Promise<TAIWorkspaceThreadMessagesResponse> {
const params = new URLSearchParams();
params.set("limit", String(options.limit || 200));
return this.get(`/api/workspaces/${workspaceSlug}/ai-workspace/threads/${threadId}/messages/?${params.toString()}`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data ?? error;
});
}
async createThreadMessage(
workspaceSlug: string,
threadId: string,
input: { role: TAIWorkspaceThreadMessageRole; content: string; payload?: Record<string, any> }
): Promise<TAIWorkspaceThreadMessageResponse> {
return this.post(`/api/workspaces/${workspaceSlug}/ai-workspace/threads/${threadId}/messages/`, input)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data ?? error;
});
}
}