feat: complete ops ai workspace bridge
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,8 @@ import type {
|
||||
TAIWorkspaceExecutorConnectionMode,
|
||||
TAIWorkspaceExecutorInput,
|
||||
TAIWorkspaceExecutorListResponse,
|
||||
TAIWorkspaceOpsProject,
|
||||
TAIWorkspaceOpsWorkspace,
|
||||
} from "@/services/workspace-ai-workspace.service";
|
||||
|
||||
const DEFAULT_WINDOWS_AGENT_PORT = "8787";
|
||||
@@ -43,8 +45,15 @@ type TExecutorDraft = {
|
||||
|
||||
type TAIWorkspaceProductSettingsModalProps = {
|
||||
isOpen: boolean;
|
||||
workspaceSlug: string;
|
||||
executors: TAIWorkspaceExecutor[];
|
||||
selectedExecutorId?: string | null;
|
||||
workspaceOptions: TAIWorkspaceOpsWorkspace[];
|
||||
selectedOpsWorkspaceSlug: string;
|
||||
workspaceLoadError?: string;
|
||||
projectOptions: TAIWorkspaceOpsProject[];
|
||||
selectedProjectId: string;
|
||||
projectLoadError?: string;
|
||||
isLoading?: boolean;
|
||||
error?: unknown;
|
||||
notice?: string;
|
||||
@@ -53,11 +62,17 @@ type TAIWorkspaceProductSettingsModalProps = {
|
||||
onClose: () => void;
|
||||
onReload: () => Promise<unknown> | unknown;
|
||||
onSelect: (executor: TAIWorkspaceExecutor) => Promise<unknown> | unknown;
|
||||
onCheck: (executor: TAIWorkspaceExecutor) => Promise<TAIWorkspaceExecutorListResponse | null>;
|
||||
onCreate: (input: TAIWorkspaceExecutorInput) => Promise<TAIWorkspaceExecutorListResponse | null>;
|
||||
onUpdate: (executorId: string, input: TAIWorkspaceExecutorInput) => Promise<TAIWorkspaceExecutorListResponse | null>;
|
||||
onDelete: (executorId: string) => Promise<TAIWorkspaceExecutorListResponse | null>;
|
||||
onOpsWorkspaceChange: (workspaceSlug: string) => void;
|
||||
onProjectChange: (projectId: string) => void;
|
||||
onDownloadAgent: (executorId: string, input: TAIWorkspaceExecutorInput, port: string) => Promise<void>;
|
||||
getWindowsAgentInstallerUrl: (executorId: string, options?: { port?: string | number }) => string;
|
||||
getWindowsAgentInstallerUrl: (
|
||||
executorId: string,
|
||||
options?: { port?: string | number; opsWorkspaceSlug?: string; opsProjectId?: string }
|
||||
) => string;
|
||||
};
|
||||
|
||||
function emptyDraft(): TExecutorDraft {
|
||||
@@ -108,11 +123,37 @@ function statusLabel(status: string | null | undefined) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
function formatCheckTime(value: string | null | undefined) {
|
||||
if (!value) return "";
|
||||
const time = Date.parse(value);
|
||||
if (!Number.isFinite(time)) return "";
|
||||
return new Date(time).toLocaleString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
if (!error) return "";
|
||||
return String((error as any)?.message || (error as any)?.error || error);
|
||||
}
|
||||
|
||||
function projectLabel(project: TAIWorkspaceOpsProject | null | undefined) {
|
||||
if (!project) return "";
|
||||
return String(project.title || project.name || project.identifier || project.projectIdentifier || project.id || "").trim();
|
||||
}
|
||||
|
||||
function projectIdentifier(project: TAIWorkspaceOpsProject | null | undefined) {
|
||||
return String(project?.identifier || project?.projectIdentifier || "").trim();
|
||||
}
|
||||
|
||||
function workspaceLabel(workspace: TAIWorkspaceOpsWorkspace | null | undefined) {
|
||||
if (!workspace) return "";
|
||||
return String(workspace.title || workspace.name || workspace.slug || workspace.id || "").trim();
|
||||
}
|
||||
|
||||
function AIWorkspaceGlassSelect<T extends string>({
|
||||
value,
|
||||
options,
|
||||
@@ -260,8 +301,15 @@ function AIWorkspaceGlassSelect<T extends string>({
|
||||
|
||||
export function AIWorkspaceProductSettingsModal({
|
||||
isOpen,
|
||||
workspaceSlug,
|
||||
executors,
|
||||
selectedExecutorId,
|
||||
workspaceOptions,
|
||||
selectedOpsWorkspaceSlug,
|
||||
workspaceLoadError,
|
||||
projectOptions,
|
||||
selectedProjectId,
|
||||
projectLoadError,
|
||||
isLoading,
|
||||
error,
|
||||
notice,
|
||||
@@ -270,9 +318,12 @@ export function AIWorkspaceProductSettingsModal({
|
||||
onClose,
|
||||
onReload,
|
||||
onSelect,
|
||||
onCheck,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onOpsWorkspaceChange,
|
||||
onProjectChange,
|
||||
onDownloadAgent,
|
||||
getWindowsAgentInstallerUrl,
|
||||
}: TAIWorkspaceProductSettingsModalProps) {
|
||||
@@ -290,6 +341,37 @@ export function AIWorkspaceProductSettingsModal({
|
||||
);
|
||||
const detailsOpen = detailsMode === "new" || detailsMode === "edit";
|
||||
const isBusy = Boolean(busyId);
|
||||
const selectedProject = useMemo(
|
||||
() => projectOptions.find((project) => project.id === selectedProjectId) || null,
|
||||
[projectOptions, selectedProjectId]
|
||||
);
|
||||
const selectedOpsWorkspace = useMemo(
|
||||
() => workspaceOptions.find((workspace) => workspace.slug === selectedOpsWorkspaceSlug) || null,
|
||||
[selectedOpsWorkspaceSlug, workspaceOptions]
|
||||
);
|
||||
const workspaceSelectOptions = useMemo(
|
||||
() =>
|
||||
workspaceOptions.map((workspace) => ({
|
||||
value: workspace.slug,
|
||||
label: workspaceLabel(workspace),
|
||||
})),
|
||||
[workspaceOptions]
|
||||
);
|
||||
const projectSelectOptions = useMemo(
|
||||
() => [
|
||||
{ value: "", label: "Не выбран" },
|
||||
...projectOptions.map((project) => {
|
||||
const identifier = projectIdentifier(project);
|
||||
const label = projectLabel(project);
|
||||
return {
|
||||
value: project.id,
|
||||
label: identifier ? `${label} · ${identifier}` : label,
|
||||
};
|
||||
}),
|
||||
],
|
||||
[projectOptions]
|
||||
);
|
||||
const contextReady = Boolean(activeExecutor && selectedOpsWorkspace && selectedProject);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@@ -321,6 +403,11 @@ export function AIWorkspaceProductSettingsModal({
|
||||
setDraft(draftFromExecutor(activeExecutor));
|
||||
};
|
||||
|
||||
const checkActive = () => {
|
||||
if (!activeExecutor) return;
|
||||
void onCheck(activeExecutor);
|
||||
};
|
||||
|
||||
const saveDraft = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const input = toInput(draft);
|
||||
@@ -363,6 +450,10 @@ export function AIWorkspaceProductSettingsModal({
|
||||
await onDownloadAgent(editingExecutor.id, toInput(draft), draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT);
|
||||
};
|
||||
|
||||
const activeCheckDetails = activeExecutor
|
||||
? [formatCheckTime(activeExecutor.lastSeenAt), activeExecutor.statusDetail].filter(Boolean).join(" · ")
|
||||
: "";
|
||||
|
||||
return (
|
||||
<ModalCore
|
||||
isOpen={isOpen}
|
||||
@@ -451,18 +542,62 @@ export function AIWorkspaceProductSettingsModal({
|
||||
<div>
|
||||
<span>Активное устройство</span>
|
||||
<b>{activeExecutor.name}</b>
|
||||
<small>Codex worker · Codex Remote</small>
|
||||
<small>Codex worker · Ops</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ai-settings-context-note ai-settings-context-note--remote">
|
||||
Прямое управление Codex-сеансами устройства.
|
||||
<div className="ai-settings-field-row">
|
||||
<label className="ai-settings-field">
|
||||
<span>Mode</span>
|
||||
<input className="ai-settings-input" value="Ops" readOnly />
|
||||
</label>
|
||||
<label className="ai-settings-field">
|
||||
<span>Workspace</span>
|
||||
<AIWorkspaceGlassSelect
|
||||
value={selectedOpsWorkspaceSlug}
|
||||
options={workspaceSelectOptions}
|
||||
ariaLabel="Ops workspace"
|
||||
disabled={workspaceSelectOptions.length <= 1}
|
||||
onChange={onOpsWorkspaceChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{workspaceLoadError ? <div className="ai-settings-error">{workspaceLoadError}</div> : null}
|
||||
|
||||
<label className="ai-settings-field">
|
||||
<span>Ops project</span>
|
||||
<AIWorkspaceGlassSelect
|
||||
value={selectedProjectId}
|
||||
options={projectSelectOptions}
|
||||
ariaLabel="Ops project"
|
||||
disabled={!selectedOpsWorkspaceSlug || projectOptions.length === 0}
|
||||
onChange={onProjectChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{projectLoadError ? <div className="ai-settings-error">{projectLoadError}</div> : null}
|
||||
|
||||
<div
|
||||
className="ai-settings-context-note"
|
||||
data-ready={contextReady ? "true" : undefined}
|
||||
>
|
||||
{contextReady
|
||||
? `Ops-контекст готов: ${workspaceLabel(selectedOpsWorkspace)} / ${projectLabel(selectedProject)}`
|
||||
: "Для записи задач в Ops выбери project. Чат доступен всегда, но запись задач будет ограничена выбранным проектом."}
|
||||
</div>
|
||||
|
||||
{activeCheckDetails ? (
|
||||
<div className="ai-settings-check-note">Last check: {activeCheckDetails}</div>
|
||||
) : null}
|
||||
|
||||
<div className="modal-actions ai-settings-actions">
|
||||
<button type="button" className="modal-btn" disabled={isBusy} onClick={editActive}>
|
||||
Настройки
|
||||
</button>
|
||||
<button type="button" className="modal-btn" disabled={isBusy} onClick={checkActive}>
|
||||
{busyId === `check:${activeExecutor.id}` ? "Проверка..." : "Проверить"}
|
||||
</button>
|
||||
<button type="button" className="modal-btn btn-primary" onClick={onClose}>
|
||||
Готово
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user