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

View File

@ -4,23 +4,13 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import useSWR from "swr";
import { MessageCircle } from "lucide-react"; import { MessageCircle } from "lucide-react";
// plane imports // plane imports
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
// components // components
import { AIWorkspaceProductSettingsModal } from "@/components/ai-workspace/product-settings-modal"; import { AIWorkspaceProductConsole } from "@/components/ai-workspace/product-console";
// services
import { WorkspaceAIWorkspaceService } from "@/services/workspace-ai-workspace.service";
import type {
TAIWorkspaceExecutor,
TAIWorkspaceExecutorInput,
TAIWorkspaceExecutorListResponse,
} from "@/services/workspace-ai-workspace.service";
const workspaceAIWorkspaceService = new WorkspaceAIWorkspaceService();
type TAIWorkspaceGlobalControlProps = { type TAIWorkspaceGlobalControlProps = {
workspaceSlug: string; workspaceSlug: string;
@ -29,15 +19,6 @@ type TAIWorkspaceGlobalControlProps = {
export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) { export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [dockSlot, setDockSlot] = useState<Element | null>(null); 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(() => { useEffect(() => {
if (typeof document === "undefined") return; 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 ( return (
<> <>
{dockSlot {dockSlot
@ -168,26 +57,7 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
) )
: null} : null}
<AIWorkspaceProductSettingsModal <AIWorkspaceProductConsole open={isOpen} workspaceSlug={workspaceSlug} onClose={() => setIsOpen(false)} />
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)
}
/>
</> </>
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,64 @@ export type TAIWorkspaceExecutorInput = {
status?: TAIWorkspaceExecutorStatus; 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 { export class WorkspaceAIWorkspaceService extends APIService {
constructor() { constructor() {
super(API_BASE_URL); super(API_BASE_URL);
@ -63,10 +121,15 @@ export class WorkspaceAIWorkspaceService extends APIService {
}); });
} }
async createExecutor(workspaceSlug: string, input: TAIWorkspaceExecutorInput): Promise<TAIWorkspaceExecutorListResponse> { async createExecutor(
await this.post(`/api/workspaces/${workspaceSlug}/ai-workspace/executors/`, { ...input, select: true }).catch((error) => { workspaceSlug: string,
throw error?.response?.data ?? error; 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); return this.listExecutors(workspaceSlug);
} }
@ -101,4 +164,68 @@ export class WorkspaceAIWorkspaceService extends APIService {
if (port) params.set("port", port); if (port) params.set("port", port);
return `${API_BASE_URL}/api/workspaces/${workspaceSlug}/ai-workspace/executors/${executorId}/agent/windows.ps1?${params.toString()}`; 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;
});
}
} }

View File

@ -1307,6 +1307,787 @@
} }
} }
.nodedc-ai-workspace-console {
--ai-console-control-h: 46px;
--wf-share-control-h: var(--ai-console-control-h);
--brand: rgb(var(--nodedc-card-active-rgb));
--brand-rgb: var(--nodedc-card-active-rgb);
--brand-contrast: rgb(var(--nodedc-on-card-active-rgb));
--nodedc-glass-panel-bg:
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(8, 9, 10, 0.9);
--nodedc-glass-control-bg: rgba(255, 255, 255, 0.045);
--nodedc-glass-control-hover: rgba(255, 255, 255, 0.078);
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055), 0 10px 28px rgba(0, 0, 0, 0.22);
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 28px 78px rgba(0, 0, 0, 0.48);
--nodedc-glass-outline-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.025));
position: fixed;
z-index: 26000;
display: grid;
width: min(1080px, calc(100vw - 104px));
height: min(620px, calc(100vh - 84px));
min-height: 420px;
min-width: 960px;
pointer-events: none;
}
.nodedc-ai-workspace-console[data-fullscreen="true"] {
min-width: 0;
}
.nodedc-ai-workspace-console .ai-console-shell {
position: relative;
isolation: isolate;
z-index: 1;
display: grid;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
grid-template-columns: var(--ai-threads-width, 330px) 10px minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
border-radius: 22px;
background: var(--nodedc-glass-panel-bg);
color: rgba(255, 255, 255, 0.92);
pointer-events: auto;
box-shadow: var(--nodedc-glass-panel-shadow);
-webkit-backdrop-filter: blur(64px) saturate(150%) brightness(1.08);
backdrop-filter: blur(64px) saturate(150%) brightness(1.08);
}
.nodedc-ai-workspace-console .ai-console-shell::after,
.nodedc-ai-workspace-console .ai-console-side-activity::after {
position: absolute;
inset: 0;
z-index: 2;
border-radius: inherit;
padding: 1px;
background: var(--nodedc-glass-outline-gradient);
content: "";
pointer-events: none;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.nodedc-ai-workspace-console .ai-console-header,
.nodedc-ai-workspace-console .ai-console-actions,
.nodedc-ai-workspace-console .ai-console-threads__head,
.nodedc-ai-workspace-console .ai-console-composer,
.nodedc-ai-workspace-console .ai-console-side-activity__header,
.nodedc-ai-workspace-console .ai-console-side-activity__actions {
display: flex;
align-items: center;
}
.nodedc-ai-workspace-console .ai-console-header {
min-height: var(--ai-console-control-h);
justify-content: space-between;
gap: 16px;
}
.nodedc-ai-workspace-console .ai-console-topbar {
grid-column: 1 / -1;
grid-row: 1;
padding: 18px;
}
.nodedc-ai-workspace-console .ai-console-header[data-draggable="true"] {
cursor: move;
touch-action: none;
user-select: none;
}
.nodedc-ai-workspace-console .ai-console-title-block {
min-width: 0;
}
.nodedc-ai-workspace-console .ai-console-title-block h2 {
margin: 7px 0 0;
min-width: 0;
overflow: hidden;
color: rgba(255, 255, 255, 0.96);
font-size: 18px;
font-weight: 760;
letter-spacing: 0;
line-height: 1.15;
text-overflow: ellipsis;
white-space: nowrap;
}
.nodedc-ai-workspace-console .ai-console-kicker {
color: rgba(255, 255, 255, 0.48);
font-size: 10px;
font-weight: 760;
letter-spacing: 0;
line-height: 1;
text-transform: uppercase;
}
.nodedc-ai-workspace-console .ai-console-actions {
flex: 0 0 auto;
gap: 8px;
}
.nodedc-ai-workspace-console .ai-console-icon-button {
display: inline-grid;
width: var(--ai-console-control-h);
height: var(--ai-console-control-h);
flex: 0 0 var(--ai-console-control-h);
place-items: center;
border: 0;
border-radius: 15px;
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.82);
cursor: pointer;
outline: none !important;
padding: 0;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.nodedc-ai-workspace-console .ai-console-icon-button:hover {
background: var(--nodedc-glass-control-hover);
color: #ffffff;
}
.nodedc-ai-workspace-console .ai-console-icon-button[data-active="true"] {
background: var(--brand);
color: var(--brand-contrast);
}
.nodedc-ai-workspace-console .ai-console-icon-button--sm {
width: 38px;
height: 38px;
flex-basis: 38px;
border-radius: 13px;
}
.nodedc-ai-workspace-console .ai-console-icon-button > span:not(.nodedc-glass-modal__close-mark) {
font-size: 19px;
font-weight: 560;
line-height: 1;
}
.nodedc-ai-workspace-console .ai-console-threads,
.nodedc-ai-workspace-console .ai-console-main {
min-width: 0;
min-height: 0;
}
.nodedc-ai-workspace-console .ai-console-threads {
display: grid;
grid-column: 1;
grid-row: 2;
grid-template-rows: auto minmax(0, 1fr);
gap: 14px;
padding: 0 12px 18px;
}
.nodedc-ai-workspace-console .ai-console-threads__head {
min-height: var(--ai-console-control-h);
justify-content: space-between;
gap: 10px;
padding-left: 8px;
}
.nodedc-ai-workspace-console .ai-console-threads__title {
margin-top: 7px;
color: rgba(255, 255, 255, 0.94);
font-size: 15px;
font-weight: 760;
line-height: 1.1;
}
.nodedc-ai-workspace-console .ai-console-thread-list {
display: grid;
min-height: 0;
align-content: start;
gap: 8px;
overflow: auto;
padding-right: 2px;
}
.nodedc-ai-workspace-console .ai-console-thread-list::-webkit-scrollbar,
.nodedc-ai-workspace-console .ai-console-messages::-webkit-scrollbar,
.nodedc-ai-workspace-console .ai-console-activity__log::-webkit-scrollbar {
width: 0;
height: 0;
}
.nodedc-ai-workspace-console .ai-console-thread {
position: relative;
display: grid;
min-height: 52px;
min-width: 0;
gap: 6px;
border: 0;
border-radius: 14px;
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
outline: none !important;
padding: 10px 13px;
text-align: left;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.nodedc-ai-workspace-console .ai-console-thread[data-editing="true"] {
padding-right: 38px;
}
.nodedc-ai-workspace-console .ai-console-thread:hover,
.nodedc-ai-workspace-console .ai-console-thread[data-active="true"] {
background: var(--nodedc-glass-control-hover);
color: rgba(255, 255, 255, 0.96);
}
.nodedc-ai-workspace-console .ai-console-thread[data-active="true"] {
background: var(--brand);
color: var(--brand-contrast);
box-shadow: none !important;
}
.nodedc-ai-workspace-console .ai-console-thread__row,
.nodedc-ai-workspace-console .ai-console-thread small,
.nodedc-ai-workspace-console .ai-console-thread__title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nodedc-ai-workspace-console .ai-console-thread__row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 760;
line-height: 1.1;
}
.nodedc-ai-workspace-console .ai-console-thread__title-input {
width: 100%;
min-width: 0;
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
font: inherit;
outline: none !important;
padding: 0;
caret-color: currentColor;
box-shadow: none !important;
}
.nodedc-ai-workspace-console .ai-console-thread__delete {
position: absolute;
top: 0;
right: 12px;
bottom: 0;
display: inline-grid;
width: 18px;
height: 18px;
margin: auto 0;
place-items: center;
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
font-size: 0;
line-height: 0;
opacity: 0.82;
outline: none !important;
padding: 0;
box-shadow: none !important;
}
.nodedc-ai-workspace-console .ai-console-thread__delete::before,
.nodedc-ai-workspace-console .ai-console-thread__delete::after {
position: absolute;
top: 50%;
left: 50%;
width: 10px;
height: 1.5px;
border-radius: 999px;
background: currentColor;
content: "";
transform: translate(-50%, -50%) rotate(45deg);
}
.nodedc-ai-workspace-console .ai-console-thread__delete::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.nodedc-ai-workspace-console .ai-console-thread small {
color: rgba(255, 255, 255, 0.44);
font-size: 11px;
line-height: 1;
}
.nodedc-ai-workspace-console .ai-console-thread[data-active="true"] small {
color: rgba(11, 17, 23, 0.68);
}
.nodedc-ai-workspace-console .ai-console-thread__status,
.nodedc-ai-workspace-console .ai-console-thinking span,
.nodedc-ai-workspace-console .ai-console-process__head i {
display: inline-block;
width: 7px;
height: 7px;
flex: 0 0 7px;
border-radius: 999px;
background: currentColor;
opacity: 0.9;
animation: ai-console-thinking-pulse 1.55s ease-in-out infinite;
}
.nodedc-ai-workspace-console .ai-console-resizer {
position: relative;
grid-column: 2;
grid-row: 2;
width: 10px;
min-width: 10px;
height: 100%;
cursor: col-resize;
}
.nodedc-ai-workspace-console .ai-console-resizer::before {
position: absolute;
top: 18px;
bottom: 18px;
left: 50%;
width: 1px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
content: "";
transform: translateX(-50%);
}
.nodedc-ai-workspace-console .ai-console-resizer:hover::before {
width: 3px;
background: rgba(255, 255, 255, 0.16);
}
.nodedc-ai-workspace-console .ai-console-main {
display: grid;
grid-column: 3;
grid-row: 2;
grid-template-rows: minmax(0, 1fr) auto;
gap: 14px;
padding: 0 18px 18px;
}
.nodedc-ai-workspace-console .ai-console-messages {
display: flex;
min-height: 0;
flex-direction: column;
gap: 10px;
overflow: auto;
padding: 4px 2px;
}
.nodedc-ai-workspace-console .ai-console-message,
.nodedc-ai-workspace-console .ai-console-process {
display: grid;
width: min(76%, 560px);
min-width: 0;
gap: 8px;
align-self: flex-start;
border-radius: 18px;
background: var(--nodedc-glass-control-bg);
padding: 12px 14px;
box-shadow: var(--nodedc-glass-control-shadow);
}
.nodedc-ai-workspace-console .ai-console-message[data-role="user"] {
align-self: flex-end;
background: rgba(var(--nodedc-card-active-rgb), 0.16);
}
.nodedc-ai-workspace-console .ai-console-message__role,
.nodedc-ai-workspace-console .ai-console-process__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: rgba(255, 255, 255, 0.46);
font-size: 10px;
font-weight: 760;
line-height: 1;
text-transform: uppercase;
}
.nodedc-ai-workspace-console .ai-console-message__role time {
color: rgba(255, 255, 255, 0.34);
font: inherit;
text-transform: none;
}
.nodedc-ai-workspace-console .ai-console-message__text {
color: rgba(255, 255, 255, 0.86);
font-size: 13px;
line-height: 1.4;
overflow-wrap: anywhere;
}
.nodedc-ai-workspace-console .ai-console-rich {
display: grid;
gap: 9px;
}
.nodedc-ai-workspace-console .ai-console-rich p {
margin: 0;
white-space: pre-line;
}
.nodedc-ai-workspace-console .ai-console-process__head {
border-bottom: 1px solid rgba(255, 255, 255, 0.075);
padding-bottom: 9px;
}
.nodedc-ai-workspace-console .ai-console-process__head i {
background: var(--brand);
color: var(--brand);
}
.nodedc-ai-workspace-console .ai-console-composer {
position: relative;
min-height: 54px;
gap: 10px;
}
.nodedc-ai-workspace-console .ai-console-composer textarea {
width: 100%;
min-width: 0;
height: 54px;
min-height: 54px;
max-height: 128px;
resize: none;
border: 0 !important;
border-radius: 18px;
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.92);
font: inherit;
font-size: 13px;
line-height: 1.2;
outline: none !important;
padding: 18px 16px;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.nodedc-ai-workspace-console .ai-console-composer textarea::placeholder {
color: rgba(255, 255, 255, 0.34);
}
.nodedc-ai-workspace-console .ai-console-composer textarea:disabled {
color: rgba(255, 255, 255, 0.46);
cursor: default;
}
.nodedc-ai-workspace-console .ai-console-send {
display: inline-grid;
width: 54px;
height: 54px;
flex: 0 0 54px;
place-items: center;
border: 0;
border-radius: 18px;
background: var(--brand);
color: var(--brand-contrast);
cursor: pointer;
outline: none !important;
padding: 0;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.nodedc-ai-workspace-console .ai-console-send:disabled {
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.32);
cursor: default;
}
.nodedc-ai-workspace-console .ai-console-thinking {
display: inline-grid;
width: 22px;
height: 54px;
flex: 0 0 22px;
place-items: center;
pointer-events: none;
}
.nodedc-ai-workspace-console .ai-console-thinking span {
background: var(--brand);
color: var(--brand);
}
.nodedc-ai-workspace-console .ai-console-side-activity {
position: absolute;
top: 0;
left: calc(100% + 16px);
isolation: isolate;
z-index: 1;
display: grid;
width: 330px;
height: 100%;
min-width: 0;
grid-template-rows: auto minmax(0, 1fr);
gap: 14px;
overflow: hidden;
border-radius: 22px;
background: var(--nodedc-glass-panel-bg);
color: rgba(255, 255, 255, 0.92);
pointer-events: auto;
padding: 18px 16px 16px;
box-shadow: var(--nodedc-glass-panel-shadow);
-webkit-backdrop-filter: blur(64px) saturate(150%) brightness(1.08);
backdrop-filter: blur(64px) saturate(150%) brightness(1.08);
}
.nodedc-ai-workspace-console .ai-console-side-activity__header {
min-width: 0;
justify-content: space-between;
gap: 14px;
}
.nodedc-ai-workspace-console .ai-console-side-activity__header h3 {
margin: 6px 0 0;
color: rgba(255, 255, 255, 0.94);
font-size: 16px;
font-weight: 760;
line-height: 1.1;
}
.nodedc-ai-workspace-console .ai-console-side-activity__actions {
gap: 8px;
}
.nodedc-ai-workspace-console .ai-console-activity {
display: grid;
min-height: 0;
grid-template-rows: auto minmax(0, 1fr);
gap: 8px;
overflow: hidden;
}
.nodedc-ai-workspace-console .ai-console-activity__head {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.nodedc-ai-workspace-console .ai-console-activity__head span {
min-width: 0;
overflow: hidden;
color: rgba(var(--nodedc-card-active-rgb), 0.96);
font-size: 10px;
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.nodedc-ai-workspace-console .ai-console-activity__head small {
color: rgba(255, 255, 255, 0.42);
font-size: 10px;
font-weight: 760;
line-height: 1.25;
text-transform: uppercase;
}
.nodedc-ai-workspace-console .ai-console-activity__log {
display: grid;
min-height: 0;
align-content: start;
gap: 5px;
overflow: auto;
padding-right: 2px;
}
.nodedc-ai-workspace-console .ai-console-activity__line {
display: grid;
min-width: 0;
grid-template-columns: 72px minmax(0, 1fr);
align-items: start;
gap: 9px;
color: rgba(255, 255, 255, 0.72);
font-size: 11px;
line-height: 1.35;
}
.nodedc-ai-workspace-console .ai-console-activity__line span {
overflow: hidden;
color: rgba(255, 255, 255, 0.42);
font-weight: 760;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.nodedc-ai-workspace-console .ai-console-activity__line code {
min-width: 0;
overflow: hidden;
color: rgba(255, 255, 255, 0.76);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
text-overflow: ellipsis;
white-space: pre-wrap;
}
.nodedc-ai-workspace-console .ai-console-window-resize-layer {
position: absolute;
inset: -6px;
z-index: 4;
pointer-events: none;
}
.nodedc-ai-workspace-console .ai-console-window-resize {
position: absolute;
pointer-events: auto;
}
.nodedc-ai-workspace-console .ai-console-window-resize--top,
.nodedc-ai-workspace-console .ai-console-window-resize--bottom {
right: 14px;
left: 14px;
height: 12px;
cursor: ns-resize;
}
.nodedc-ai-workspace-console .ai-console-window-resize--top {
top: 0;
}
.nodedc-ai-workspace-console .ai-console-window-resize--bottom {
bottom: 0;
}
.nodedc-ai-workspace-console .ai-console-window-resize--left,
.nodedc-ai-workspace-console .ai-console-window-resize--right {
top: 14px;
bottom: 14px;
width: 12px;
cursor: ew-resize;
}
.nodedc-ai-workspace-console .ai-console-window-resize--left {
left: 0;
}
.nodedc-ai-workspace-console .ai-console-window-resize--right {
right: 0;
}
.nodedc-ai-workspace-console .ai-console-window-resize--top-left,
.nodedc-ai-workspace-console .ai-console-window-resize--top-right,
.nodedc-ai-workspace-console .ai-console-window-resize--bottom-right,
.nodedc-ai-workspace-console .ai-console-window-resize--bottom-left {
width: 18px;
height: 18px;
}
.nodedc-ai-workspace-console .ai-console-window-resize--top-left {
top: 0;
left: 0;
cursor: nwse-resize;
}
.nodedc-ai-workspace-console .ai-console-window-resize--top-right {
top: 0;
right: 0;
cursor: nesw-resize;
}
.nodedc-ai-workspace-console .ai-console-window-resize--bottom-right {
right: 0;
bottom: 0;
cursor: nwse-resize;
}
.nodedc-ai-workspace-console .ai-console-window-resize--bottom-left {
bottom: 0;
left: 0;
cursor: nesw-resize;
}
@keyframes ai-console-thinking-pulse {
0%,
100% {
transform: scale(0.72);
box-shadow: 0 0 0 0 rgba(var(--nodedc-card-active-rgb), 0.22);
opacity: 0.58;
}
48% {
transform: scale(1.75);
box-shadow: 0 0 0 9px rgba(var(--nodedc-card-active-rgb), 0.04);
opacity: 1;
}
}
@media (max-width: 1040px) {
.nodedc-ai-workspace-console {
left: 12px !important;
top: 72px !important;
width: calc(100vw - 24px) !important;
height: calc(100vh - 88px) !important;
min-width: 0;
}
.nodedc-ai-workspace-console .ai-console-shell {
grid-template-columns: minmax(190px, 35vw) 8px minmax(0, 1fr);
}
}
@media (max-width: 760px) {
.nodedc-ai-workspace-console .ai-console-shell {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto 148px minmax(0, 1fr);
}
.nodedc-ai-workspace-console .ai-console-topbar {
grid-column: 1;
grid-row: 1;
padding: 12px;
}
.nodedc-ai-workspace-console .ai-console-threads {
grid-column: 1;
grid-row: 2;
padding: 0 12px 10px;
}
.nodedc-ai-workspace-console .ai-console-resizer {
display: none;
}
.nodedc-ai-workspace-console .ai-console-main {
grid-column: 1;
grid-row: 3;
padding: 0 12px 12px;
}
.nodedc-ai-workspace-console .ai-console-title-block h2 {
max-width: 42vw;
}
.nodedc-ai-workspace-console .ai-console-side-activity {
right: 0;
left: 0;
width: 100%;
height: min(280px, calc(100vh - 96px));
transform: translateY(calc(-100% - 12px));
}
.nodedc-ai-workspace-console .ai-console-window-resize-layer {
display: none;
}
}
.nodedc-bottom-dock-aware-padding { .nodedc-bottom-dock-aware-padding {
padding-bottom: calc(var(--nodedc-quick-add-reserve, 2.5rem) + 0.5rem); padding-bottom: calc(var(--nodedc-quick-add-reserve, 2.5rem) + 0.5rem);
} }