Prevent streaming updates from re-rendering session UI
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { Component, For, createSignal, createEffect, Show, onMount, onCleanup } from "solid-js"
|
import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js"
|
||||||
import { instances } from "../stores/instances"
|
import { instances, getInstanceLogs } from "../stores/instances"
|
||||||
import { ChevronDown } from "lucide-solid"
|
import { ChevronDown } from "lucide-solid"
|
||||||
import type { LogEntry } from "../types/instance"
|
|
||||||
import InstanceInfo from "./instance-info"
|
import InstanceInfo from "./instance-info"
|
||||||
|
|
||||||
interface InfoViewProps {
|
interface InfoViewProps {
|
||||||
@@ -16,7 +15,7 @@ const InfoView: Component<InfoViewProps> = (props) => {
|
|||||||
const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false)
|
const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false)
|
||||||
|
|
||||||
const instance = () => instances().get(props.instanceId)
|
const instance = () => instances().get(props.instanceId)
|
||||||
const logs = () => instance()?.logs ?? []
|
const logs = createMemo(() => getInstanceLogs(props.instanceId))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (scrollRef && savedState) {
|
if (scrollRef && savedState) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Component, For, createSignal, createEffect, Show, onMount, onCleanup } from "solid-js"
|
import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js"
|
||||||
import { instances } from "../stores/instances"
|
import { instances, getInstanceLogs } from "../stores/instances"
|
||||||
import { Trash2, ChevronDown } from "lucide-solid"
|
import { ChevronDown } from "lucide-solid"
|
||||||
import type { LogEntry } from "../types/instance"
|
|
||||||
|
|
||||||
interface LogsViewProps {
|
interface LogsViewProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
@@ -15,7 +14,7 @@ const LogsView: Component<LogsViewProps> = (props) => {
|
|||||||
const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false)
|
const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false)
|
||||||
|
|
||||||
const instance = () => instances().get(props.instanceId)
|
const instance = () => instances().get(props.instanceId)
|
||||||
const logs = () => instance()?.logs ?? []
|
const logs = createMemo(() => getInstanceLogs(props.instanceId))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (scrollRef && savedState) {
|
if (scrollRef && savedState) {
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ interface MessageCacheEntry {
|
|||||||
interface ToolCacheEntry {
|
interface ToolCacheEntry {
|
||||||
toolPart: any
|
toolPart: any
|
||||||
messageInfo?: any
|
messageInfo?: any
|
||||||
|
signature: string
|
||||||
item: ToolDisplayItem
|
item: ToolDisplayItem
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +171,13 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
const scrollStateKey = () => makeScrollKey(props.instanceId, props.sessionId)
|
const scrollStateKey = () => makeScrollKey(props.instanceId, props.sessionId)
|
||||||
const connectionStatus = () => sseManager.getStatus(props.instanceId)
|
const connectionStatus = () => sseManager.getStatus(props.instanceId)
|
||||||
|
|
||||||
|
function createToolSignature(message: Message, toolPart: any, toolIndex: number, messageInfo?: any): string {
|
||||||
|
const messageId = message.id
|
||||||
|
const partId = typeof toolPart?.id === "string" ? toolPart.id : `${messageId}-tool-${toolIndex}`
|
||||||
|
const status = toolPart?.state?.status ?? messageInfo?.state?.status ?? ""
|
||||||
|
const version = message.version ?? 0
|
||||||
|
return `${messageId}:${partId}:${status}:${version}`
|
||||||
|
}
|
||||||
|
|
||||||
const sessionInfo = createMemo(() => {
|
const sessionInfo = createMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -344,12 +352,14 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
const toolPart = displayParts.tool[toolIndex]
|
const toolPart = displayParts.tool[toolIndex]
|
||||||
const toolKey = typeof toolPart?.id === "string" ? toolPart.id : `${message.id}-tool-${toolIndex}`
|
const toolKey = typeof toolPart?.id === "string" ? toolPart.id : `${message.id}-tool-${toolIndex}`
|
||||||
|
|
||||||
|
const toolSignature = createToolSignature(message, toolPart, toolIndex, messageInfo)
|
||||||
const toolEntry = toolItemCache.get(toolKey)
|
const toolEntry = toolItemCache.get(toolKey)
|
||||||
if (toolEntry && toolEntry.toolPart === toolPart && toolEntry.messageInfo === messageInfo) {
|
if (toolEntry && toolEntry.signature === toolSignature) {
|
||||||
toolEntry.item.toolPart = toolPart
|
|
||||||
toolEntry.item.messageInfo = messageInfo
|
|
||||||
toolEntry.toolPart = toolPart
|
toolEntry.toolPart = toolPart
|
||||||
toolEntry.messageInfo = messageInfo
|
toolEntry.messageInfo = messageInfo
|
||||||
|
toolEntry.signature = toolSignature
|
||||||
|
toolEntry.item.toolPart = toolPart
|
||||||
|
toolEntry.item.messageInfo = messageInfo
|
||||||
newToolCache.set(toolKey, toolEntry)
|
newToolCache.set(toolKey, toolEntry)
|
||||||
items.push(toolEntry.item)
|
items.push(toolEntry.item)
|
||||||
} else {
|
} else {
|
||||||
@@ -359,7 +369,7 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
toolPart,
|
toolPart,
|
||||||
messageInfo,
|
messageInfo,
|
||||||
}
|
}
|
||||||
newToolCache.set(toolKey, { toolPart, messageInfo, item: toolItem })
|
newToolCache.set(toolKey, { toolPart, messageInfo, signature: toolSignature, item: toolItem })
|
||||||
items.push(toolItem)
|
items.push(toolItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,6 @@ import { MessageSquare, Info, Plus, X } from "lucide-solid"
|
|||||||
import KeyboardHint from "./keyboard-hint"
|
import KeyboardHint from "./keyboard-hint"
|
||||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||||
|
|
||||||
interface SessionListItem {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
isSpecial?: boolean
|
|
||||||
isActive: boolean
|
|
||||||
isParent?: boolean
|
|
||||||
onSelect: () => void
|
|
||||||
onClose?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionListProps {
|
interface SessionListProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessions: Map<string, Session>
|
sessions: Map<string, Session>
|
||||||
@@ -33,6 +23,24 @@ const MAX_WIDTH = 500
|
|||||||
const DEFAULT_WIDTH = 280
|
const DEFAULT_WIDTH = 280
|
||||||
const STORAGE_KEY = "opencode-session-sidebar-width"
|
const STORAGE_KEY = "opencode-session-sidebar-width"
|
||||||
|
|
||||||
|
function arraysEqual(prev: readonly string[] | undefined, next: readonly string[]): boolean {
|
||||||
|
if (!prev) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev.length !== next.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < prev.length; i++) {
|
||||||
|
if (prev[i] !== next[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const SessionList: Component<SessionListProps> = (props) => {
|
const SessionList: Component<SessionListProps> = (props) => {
|
||||||
const [sidebarWidth, setSidebarWidth] = createSignal(DEFAULT_WIDTH)
|
const [sidebarWidth, setSidebarWidth] = createSignal(DEFAULT_WIDTH)
|
||||||
const [isResizing, setIsResizing] = createSignal(false)
|
const [isResizing, setIsResizing] = createSignal(false)
|
||||||
@@ -151,44 +159,38 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
removeTouchListeners()
|
removeTouchListeners()
|
||||||
})
|
})
|
||||||
|
|
||||||
const sessionSections = createMemo(() => {
|
const parentSessionIds = createMemo(
|
||||||
const parentItems: SessionListItem[] = []
|
() => {
|
||||||
const childItems: SessionListItem[] = []
|
const ids: string[] = []
|
||||||
|
for (const session of props.sessions.values()) {
|
||||||
for (const [id, session] of props.sessions.entries()) {
|
if (session.parentId === null) {
|
||||||
const item: SessionListItem = {
|
ids.push(session.id)
|
||||||
id,
|
}
|
||||||
title: session.title || "Untitled",
|
|
||||||
isActive: id === props.activeSessionId,
|
|
||||||
isParent: session.parentId === null,
|
|
||||||
onSelect: () => props.onSelect(id),
|
|
||||||
onClose: session.parentId === null ? () => props.onClose(id) : undefined,
|
|
||||||
}
|
}
|
||||||
|
ids.push("info")
|
||||||
|
return ids
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ equals: arraysEqual },
|
||||||
|
)
|
||||||
|
|
||||||
if (session.parentId === null) {
|
const childSessionIds = createMemo(
|
||||||
parentItems.push(item)
|
() => {
|
||||||
} else {
|
const children: { id: string; updated: number }[] = []
|
||||||
childItems.push(item)
|
for (const session of props.sessions.values()) {
|
||||||
|
if (session.parentId !== null) {
|
||||||
|
children.push({ id: session.id, updated: session.time.updated ?? 0 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (children.length <= 1) {
|
||||||
|
return children.map((entry) => entry.id)
|
||||||
childItems.sort((a, b) => {
|
}
|
||||||
const sessionA = props.sessions.get(a.id)
|
children.sort((a, b) => b.updated - a.updated)
|
||||||
const sessionB = props.sessions.get(b.id)
|
return children.map((entry) => entry.id)
|
||||||
if (!sessionA || !sessionB) return 0
|
},
|
||||||
return sessionB.time.updated - sessionA.time.updated
|
undefined,
|
||||||
})
|
{ equals: arraysEqual },
|
||||||
|
)
|
||||||
parentItems.push({
|
|
||||||
id: "info",
|
|
||||||
title: "Info",
|
|
||||||
isSpecial: true,
|
|
||||||
isActive: props.activeSessionId === "info",
|
|
||||||
onSelect: () => props.onSelect("info"),
|
|
||||||
})
|
|
||||||
|
|
||||||
return { parentItems, childItems }
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -221,30 +223,50 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||||
User Session & Info
|
User Session & Info
|
||||||
</div>
|
</div>
|
||||||
<For each={sessionSections().parentItems}>
|
<For each={parentSessionIds()}>
|
||||||
{(item) => (
|
{(id) => {
|
||||||
<div class="session-list-item group">
|
if (id === "info") {
|
||||||
<button
|
const isActive = () => props.activeSessionId === "info"
|
||||||
class={`session-item-base ${
|
return (
|
||||||
item.isActive ? "session-item-active" : "session-item-inactive"
|
<div class="session-list-item group">
|
||||||
} ${item.isSpecial ? "session-item-special" : ""}`}
|
<button
|
||||||
onClick={item.onSelect}
|
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"} session-item-special`}
|
||||||
title={item.title}
|
onClick={() => props.onSelect("info")}
|
||||||
role="button"
|
title="Info"
|
||||||
aria-selected={item.isActive}
|
role="button"
|
||||||
>
|
aria-selected={isActive()}
|
||||||
<Show when={item.isSpecial} fallback={<MessageSquare class="w-4 h-4 flex-shrink-0" />}>
|
>
|
||||||
<Info class="w-4 h-4 flex-shrink-0" />
|
<Info class="w-4 h-4 flex-shrink-0" />
|
||||||
</Show>
|
<span class="session-item-title truncate">Info</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<span class="session-item-title truncate">{item.title}</span>
|
const session = () => props.sessions.get(id)
|
||||||
|
if (!session()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
<Show when={!item.isSpecial && item.onClose}>
|
const isActive = () => props.activeSessionId === id
|
||||||
|
const title = () => session()?.title || "Untitled"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="session-list-item group">
|
||||||
|
<button
|
||||||
|
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||||
|
onClick={() => props.onSelect(id)}
|
||||||
|
title={title()}
|
||||||
|
role="button"
|
||||||
|
aria-selected={isActive()}
|
||||||
|
>
|
||||||
|
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||||
|
<span class="session-item-title truncate">{title()}</span>
|
||||||
<span
|
<span
|
||||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
item.onClose?.()
|
props.onClose(id)
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -252,51 +274,43 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<X class="w-3 h-3" />
|
<X class="w-3 h-3" />
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={sessionSections().childItems.length > 0}>
|
<Show when={childSessionIds().length > 0}>
|
||||||
<div class="session-section">
|
<div class="session-section">
|
||||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||||
Agent Sessions
|
Agent Sessions
|
||||||
</div>
|
</div>
|
||||||
<For each={sessionSections().childItems}>
|
<For each={childSessionIds()}>
|
||||||
{(item) => (
|
{(id) => {
|
||||||
<div class="session-list-item group">
|
const session = () => props.sessions.get(id)
|
||||||
<button
|
if (!session()) {
|
||||||
class={`session-item-base ${
|
return null
|
||||||
item.isActive ? "session-item-active" : "session-item-inactive"
|
}
|
||||||
} ${item.isSpecial ? "session-item-special" : ""}`}
|
|
||||||
onClick={item.onSelect}
|
|
||||||
title={item.title}
|
|
||||||
role="button"
|
|
||||||
aria-selected={item.isActive}
|
|
||||||
>
|
|
||||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
|
||||||
|
|
||||||
<span class="session-item-title truncate">{item.title}</span>
|
const isActive = () => props.activeSessionId === id
|
||||||
|
const title = () => session()?.title || "Untitled"
|
||||||
|
|
||||||
<Show when={!item.isSpecial && item.onClose}>
|
return (
|
||||||
<span
|
<div class="session-list-item group">
|
||||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
<button
|
||||||
onClick={(event) => {
|
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||||
event.stopPropagation()
|
onClick={() => props.onSelect(id)}
|
||||||
item.onClose?.()
|
title={title()}
|
||||||
}}
|
role="button"
|
||||||
role="button"
|
aria-selected={isActive()}
|
||||||
tabIndex={0}
|
>
|
||||||
aria-label="Close session"
|
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||||
>
|
<span class="session-item-title truncate">{title()}</span>
|
||||||
<X class="w-3 h-3" />
|
</button>
|
||||||
</span>
|
</div>
|
||||||
</Show>
|
)
|
||||||
</button>
|
}}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -7,15 +7,43 @@ import { preferences, updateLastUsedBinary } from "./preferences"
|
|||||||
|
|
||||||
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
|
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
|
||||||
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
|
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
|
||||||
|
const [instanceLogs, setInstanceLogs] = createSignal<Map<string, LogEntry[]>>(new Map())
|
||||||
|
|
||||||
const MAX_LOG_ENTRIES = 1000
|
const MAX_LOG_ENTRIES = 1000
|
||||||
|
|
||||||
|
function ensureLogContainer(id: string) {
|
||||||
|
setInstanceLogs((prev) => {
|
||||||
|
if (prev.has(id)) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(id, [])
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLogContainer(id: string) {
|
||||||
|
setInstanceLogs((prev) => {
|
||||||
|
if (!prev.has(id)) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.delete(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstanceLogs(instanceId: string): LogEntry[] {
|
||||||
|
return instanceLogs().get(instanceId) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
function addInstance(instance: Instance) {
|
function addInstance(instance: Instance) {
|
||||||
setInstances((prev) => {
|
setInstances((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
next.set(instance.id, instance)
|
next.set(instance.id, instance)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
ensureLogContainer(instance.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInstance(id: string, updates: Partial<Instance>) {
|
function updateInstance(id: string, updates: Partial<Instance>) {
|
||||||
@@ -35,6 +63,7 @@ function removeInstance(id: string) {
|
|||||||
next.delete(id)
|
next.delete(id)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
removeLogContainer(id)
|
||||||
|
|
||||||
if (activeInstanceId() === id) {
|
if (activeInstanceId() === id) {
|
||||||
setActiveInstanceId(null)
|
setActiveInstanceId(null)
|
||||||
@@ -54,7 +83,6 @@ async function createInstance(folder: string, binaryPath?: string): Promise<stri
|
|||||||
pid: 0,
|
pid: 0,
|
||||||
status: "starting",
|
status: "starting",
|
||||||
client: null,
|
client: null,
|
||||||
logs: [],
|
|
||||||
environmentVariables: preferences().environmentVariables,
|
environmentVariables: preferences().environmentVariables,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,27 +155,22 @@ function getActiveInstance(): Instance | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addLog(id: string, entry: LogEntry) {
|
function addLog(id: string, entry: LogEntry) {
|
||||||
setInstances((prev) => {
|
setInstanceLogs((prev) => {
|
||||||
const next = new Map(prev)
|
const next = new Map(prev)
|
||||||
const instance = next.get(id)
|
const existing = next.get(id) ?? []
|
||||||
if (instance) {
|
const updated = existing.length >= MAX_LOG_ENTRIES ? [...existing.slice(1), entry] : [...existing, entry]
|
||||||
const logs = [...instance.logs, entry]
|
next.set(id, updated)
|
||||||
if (logs.length > MAX_LOG_ENTRIES) {
|
|
||||||
logs.shift()
|
|
||||||
}
|
|
||||||
next.set(id, { ...instance, logs })
|
|
||||||
}
|
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLogs(id: string) {
|
function clearLogs(id: string) {
|
||||||
setInstances((prev) => {
|
setInstanceLogs((prev) => {
|
||||||
const next = new Map(prev)
|
if (!prev.has(id)) {
|
||||||
const instance = next.get(id)
|
return prev
|
||||||
if (instance) {
|
|
||||||
next.set(id, { ...instance, logs: [] })
|
|
||||||
}
|
}
|
||||||
|
const next = new Map(prev)
|
||||||
|
next.set(id, [])
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -164,4 +187,6 @@ export {
|
|||||||
getActiveInstance,
|
getActiveInstance,
|
||||||
addLog,
|
addLog,
|
||||||
clearLogs,
|
clearLogs,
|
||||||
|
instanceLogs,
|
||||||
|
getInstanceLogs,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export interface Instance {
|
|||||||
status: "starting" | "ready" | "error" | "stopped"
|
status: "starting" | "ready" | "error" | "stopped"
|
||||||
error?: string
|
error?: string
|
||||||
client: OpencodeClient | null
|
client: OpencodeClient | null
|
||||||
logs: LogEntry[]
|
|
||||||
metadata?: InstanceMetadata
|
metadata?: InstanceMetadata
|
||||||
binaryPath?: string
|
binaryPath?: string
|
||||||
environmentVariables?: Record<string, string>
|
environmentVariables?: Record<string, string>
|
||||||
|
|||||||
Reference in New Issue
Block a user