fix(chat): stabilize web UI tool runs
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
|
||||
- Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments.
|
||||
- Control UI: show a reading indicator bubble while the assistant is responding.
|
||||
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
|
||||
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
|
||||
- Status: show model auth source (api-key/oauth).
|
||||
- Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split.
|
||||
|
||||
@@ -650,23 +650,10 @@ export function subscribeEmbeddedPiSession(params: {
|
||||
if (evtType === "text_end" && blockReplyBreak === "text_end") {
|
||||
if (blockChunking && blockBuffer.length > 0) {
|
||||
drainBlockBuffer(true);
|
||||
} else if (next && next !== lastBlockReplyText) {
|
||||
lastBlockReplyText = next || undefined;
|
||||
if (next) assistantTexts.push(next);
|
||||
if (next && params.onBlockReply) {
|
||||
const { text: cleanedText, mediaUrls } =
|
||||
splitMediaFromOutput(next);
|
||||
if (cleanedText || (mediaUrls && mediaUrls.length > 0)) {
|
||||
void params.onBlockReply({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (blockBuffer.length > 0) {
|
||||
emitBlockChunk(blockBuffer);
|
||||
}
|
||||
deltaBuffer = "";
|
||||
blockBuffer = "";
|
||||
lastStreamedAssistant = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ export async function handleCommands(params: {
|
||||
resolvedVerboseLevel,
|
||||
resolvedElevatedLevel,
|
||||
resolveDefaultThinkingLevel,
|
||||
provider,
|
||||
model,
|
||||
contextTokens,
|
||||
isGroup,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
type SessionEntry,
|
||||
saveSessionStore,
|
||||
} from "../config/sessions.js";
|
||||
import { registerAgentRunContext } from "../infra/agent-events.js";
|
||||
import {
|
||||
loadVoiceWakeConfig,
|
||||
setVoiceWakeTriggers,
|
||||
@@ -844,12 +845,12 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
ctx.chatAbortControllers.delete(runId);
|
||||
ctx.chatRunBuffers.delete(runId);
|
||||
ctx.chatDeltaSentAt.delete(runId);
|
||||
ctx.removeChatRun(active.sessionId, runId, sessionKey);
|
||||
ctx.removeChatRun(runId, runId, sessionKey);
|
||||
|
||||
const payload = {
|
||||
runId,
|
||||
sessionKey,
|
||||
seq: (ctx.agentRunSeq.get(active.sessionId) ?? 0) + 1,
|
||||
seq: (ctx.agentRunSeq.get(runId) ?? 0) + 1,
|
||||
state: "aborted" as const,
|
||||
};
|
||||
ctx.broadcast("chat", payload);
|
||||
@@ -940,6 +941,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
lastTo: entry?.lastTo,
|
||||
};
|
||||
const clientRunId = p.idempotencyKey;
|
||||
registerAgentRunContext(clientRunId, { sessionKey: p.sessionKey });
|
||||
|
||||
const cached = ctx.dedupe.get(`chat:${clientRunId}`);
|
||||
if (cached) {
|
||||
@@ -962,7 +964,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
sessionId,
|
||||
sessionKey: p.sessionKey,
|
||||
});
|
||||
ctx.addChatRun(sessionId, {
|
||||
ctx.addChatRun(clientRunId, {
|
||||
sessionKey: p.sessionKey,
|
||||
clientRunId,
|
||||
});
|
||||
@@ -978,6 +980,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
{
|
||||
message: messageWithAttachments,
|
||||
sessionId,
|
||||
runId: clientRunId,
|
||||
thinking: p.thinking,
|
||||
deliver: p.deliver,
|
||||
timeout: Math.ceil(timeoutMs / 1000).toString(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { resolveThinkingDefault } from "../../agents/model-selection.js";
|
||||
import { agentCommand } from "../../commands/agent.js";
|
||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { buildMessageWithAttachments } from "../chat-attachments.js";
|
||||
@@ -115,12 +116,12 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
context.chatAbortControllers.delete(runId);
|
||||
context.chatRunBuffers.delete(runId);
|
||||
context.chatDeltaSentAt.delete(runId);
|
||||
context.removeChatRun(active.sessionId, runId, sessionKey);
|
||||
context.removeChatRun(runId, runId, sessionKey);
|
||||
|
||||
const payload = {
|
||||
runId,
|
||||
sessionKey,
|
||||
seq: (context.agentRunSeq.get(active.sessionId) ?? 0) + 1,
|
||||
seq: (context.agentRunSeq.get(runId) ?? 0) + 1,
|
||||
state: "aborted" as const,
|
||||
};
|
||||
context.broadcast("chat", payload);
|
||||
@@ -201,6 +202,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
lastTo: entry?.lastTo,
|
||||
};
|
||||
const clientRunId = p.idempotencyKey;
|
||||
registerAgentRunContext(clientRunId, { sessionKey: p.sessionKey });
|
||||
|
||||
const sendPolicy = resolveSendPolicy({
|
||||
cfg,
|
||||
@@ -236,7 +238,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
sessionId,
|
||||
sessionKey: p.sessionKey,
|
||||
});
|
||||
context.addChatRun(sessionId, {
|
||||
context.addChatRun(clientRunId, {
|
||||
sessionKey: p.sessionKey,
|
||||
clientRunId,
|
||||
});
|
||||
@@ -252,6 +254,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
{
|
||||
message: messageWithAttachments,
|
||||
sessionId,
|
||||
runId: clientRunId,
|
||||
thinking: p.thinking,
|
||||
deliver: p.deliver,
|
||||
timeout: Math.ceil(timeoutMs / 1000).toString(),
|
||||
|
||||
@@ -830,7 +830,7 @@ describe("gateway server chat", () => {
|
||||
);
|
||||
|
||||
emitAgentEvent({
|
||||
runId: "sess-main",
|
||||
runId: "idem-1",
|
||||
stream: "lifecycle",
|
||||
data: { phase: "end" },
|
||||
});
|
||||
@@ -852,7 +852,7 @@ describe("gateway server chat", () => {
|
||||
);
|
||||
|
||||
emitAgentEvent({
|
||||
runId: "sess-main",
|
||||
runId: "idem-2",
|
||||
stream: "lifecycle",
|
||||
data: { phase: "end" },
|
||||
});
|
||||
|
||||
@@ -378,16 +378,20 @@ export function renderApp(state: AppViewState) {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatRunId = null;
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({ ...state.settings, sessionKey: next });
|
||||
void loadChatHistory(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
loading: state.chatLoading,
|
||||
sending: state.chatSending,
|
||||
messages: [...state.chatMessages, ...state.chatToolMessages],
|
||||
messages: state.chatMessages,
|
||||
toolMessages: state.chatToolMessages,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
draft: state.chatMessage,
|
||||
connected: state.connected,
|
||||
canSend: state.connected,
|
||||
|
||||
@@ -178,6 +178,7 @@ export class ClawdbotApp extends LitElement {
|
||||
@state() hello: GatewayHelloOk | null = null;
|
||||
@state() lastError: string | null = null;
|
||||
@state() eventLog: EventLogEntry[] = [];
|
||||
private eventLogBuffer: EventLogEntry[] = [];
|
||||
|
||||
@state() sessionKey = this.settings.sessionKey;
|
||||
@state() chatLoading = false;
|
||||
@@ -186,6 +187,7 @@ export class ClawdbotApp extends LitElement {
|
||||
@state() chatMessages: unknown[] = [];
|
||||
@state() chatToolMessages: unknown[] = [];
|
||||
@state() chatStream: string | null = null;
|
||||
@state() chatStreamStartedAt: number | null = null;
|
||||
@state() chatRunId: string | null = null;
|
||||
@state() chatThinkingLevel: string | null = null;
|
||||
|
||||
@@ -341,6 +343,7 @@ export class ClawdbotApp extends LitElement {
|
||||
client: GatewayBrowserClient | null = null;
|
||||
private chatScrollFrame: number | null = null;
|
||||
private chatScrollTimeout: number | null = null;
|
||||
private chatHasAutoScrolled = false;
|
||||
private nodesPollInterval: number | null = null;
|
||||
private toolStreamById = new Map<string, ToolStreamEntry>();
|
||||
private toolStreamOrder: string[] = [];
|
||||
@@ -386,10 +389,14 @@ export class ClawdbotApp extends LitElement {
|
||||
changed.has("chatToolMessages") ||
|
||||
changed.has("chatStream") ||
|
||||
changed.has("chatLoading") ||
|
||||
changed.has("chatMessage") ||
|
||||
changed.has("tab"))
|
||||
) {
|
||||
this.scheduleChatScroll();
|
||||
const forcedByTab = changed.has("tab");
|
||||
const forcedByLoad =
|
||||
changed.has("chatLoading") &&
|
||||
changed.get("chatLoading") === true &&
|
||||
this.chatLoading === false;
|
||||
this.scheduleChatScroll(forcedByTab || forcedByLoad || !this.chatHasAutoScrolled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +431,7 @@ export class ClawdbotApp extends LitElement {
|
||||
this.client.start();
|
||||
}
|
||||
|
||||
private scheduleChatScroll() {
|
||||
private scheduleChatScroll(force = false) {
|
||||
if (this.chatScrollFrame) cancelAnimationFrame(this.chatScrollFrame);
|
||||
if (this.chatScrollTimeout != null) {
|
||||
clearTimeout(this.chatScrollTimeout);
|
||||
@@ -434,11 +441,19 @@ export class ClawdbotApp extends LitElement {
|
||||
this.chatScrollFrame = null;
|
||||
const container = this.querySelector(".chat-thread") as HTMLElement | null;
|
||||
if (!container) return;
|
||||
const distanceFromBottom =
|
||||
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
const shouldStick = force || distanceFromBottom < 140;
|
||||
if (!shouldStick) return;
|
||||
if (force) this.chatHasAutoScrolled = true;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
this.chatScrollTimeout = window.setTimeout(() => {
|
||||
this.chatScrollTimeout = null;
|
||||
const latest = this.querySelector(".chat-thread") as HTMLElement | null;
|
||||
if (!latest) return;
|
||||
const latestDistanceFromBottom =
|
||||
latest.scrollHeight - latest.scrollTop - latest.clientHeight;
|
||||
if (!force && latestDistanceFromBottom >= 180) return;
|
||||
latest.scrollTop = latest.scrollHeight;
|
||||
}, 120);
|
||||
});
|
||||
@@ -477,6 +492,10 @@ export class ClawdbotApp extends LitElement {
|
||||
this.chatToolMessages = [];
|
||||
}
|
||||
|
||||
resetChatScroll() {
|
||||
this.chatHasAutoScrolled = false;
|
||||
}
|
||||
|
||||
private trimToolStream() {
|
||||
if (this.toolStreamOrder.length <= TOOL_STREAM_LIMIT) return;
|
||||
const overflow = this.toolStreamOrder.length - TOOL_STREAM_LIMIT;
|
||||
@@ -520,6 +539,8 @@ export class ClawdbotApp extends LitElement {
|
||||
if (sessionKey && sessionKey !== this.sessionKey) return;
|
||||
// Fallback: only accept session-less events for the active run.
|
||||
if (!sessionKey && this.chatRunId && payload.runId !== this.chatRunId) return;
|
||||
if (this.chatRunId && payload.runId !== this.chatRunId) return;
|
||||
if (!this.chatRunId) return;
|
||||
|
||||
const data = payload.data ?? {};
|
||||
const toolCallId =
|
||||
@@ -564,10 +585,13 @@ export class ClawdbotApp extends LitElement {
|
||||
}
|
||||
|
||||
private onEvent(evt: GatewayEventFrame) {
|
||||
this.eventLog = [
|
||||
this.eventLogBuffer = [
|
||||
{ ts: Date.now(), event: evt.event, payload: evt.payload },
|
||||
...this.eventLog,
|
||||
...this.eventLogBuffer,
|
||||
].slice(0, 250);
|
||||
if (this.tab === "debug") {
|
||||
this.eventLog = this.eventLogBuffer;
|
||||
}
|
||||
|
||||
if (evt.event === "agent") {
|
||||
this.handleAgentEvent(evt.payload as AgentEventPayload | undefined);
|
||||
@@ -577,6 +601,9 @@ export class ClawdbotApp extends LitElement {
|
||||
if (evt.event === "chat") {
|
||||
const payload = evt.payload as ChatEventPayload | undefined;
|
||||
const state = handleChatEvent(this, payload);
|
||||
if (state === "final" || state === "error" || state === "aborted") {
|
||||
this.resetToolStream();
|
||||
}
|
||||
if (state === "final") void loadChatHistory(this);
|
||||
return;
|
||||
}
|
||||
@@ -633,6 +660,7 @@ export class ClawdbotApp extends LitElement {
|
||||
|
||||
setTab(next: Tab) {
|
||||
if (this.tab !== next) this.tab = next;
|
||||
if (next === "chat") this.chatHasAutoScrolled = false;
|
||||
void this.refreshActiveTab();
|
||||
this.syncUrlWithTab(next, false);
|
||||
}
|
||||
@@ -667,7 +695,10 @@ export class ClawdbotApp extends LitElement {
|
||||
await loadConfigSchema(this);
|
||||
await loadConfig(this);
|
||||
}
|
||||
if (this.tab === "debug") await loadDebug(this);
|
||||
if (this.tab === "debug") {
|
||||
await loadDebug(this);
|
||||
this.eventLog = this.eventLogBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
private inferBasePath() {
|
||||
@@ -740,6 +771,7 @@ export class ClawdbotApp extends LitElement {
|
||||
|
||||
private setTabFromRoute(next: Tab) {
|
||||
if (this.tab !== next) this.tab = next;
|
||||
if (next === "chat") this.chatHasAutoScrolled = false;
|
||||
if (this.connected) void this.refreshActiveTab();
|
||||
}
|
||||
|
||||
@@ -776,8 +808,16 @@ export class ClawdbotApp extends LitElement {
|
||||
}
|
||||
async handleSendChat() {
|
||||
if (!this.connected) return;
|
||||
this.resetToolStream();
|
||||
const ok = await sendChat(this);
|
||||
if (ok) void loadChatHistory(this);
|
||||
if (ok && this.chatRunId) {
|
||||
// chat.send returned (run finished), but we missed the chat final event.
|
||||
this.chatRunId = null;
|
||||
this.chatStream = null;
|
||||
this.chatStreamStartedAt = null;
|
||||
this.resetToolStream();
|
||||
void loadChatHistory(this);
|
||||
}
|
||||
this.scheduleChatScroll();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ChatState = {
|
||||
chatMessage: string;
|
||||
chatRunId: string | null;
|
||||
chatStream: string | null;
|
||||
chatStreamStartedAt: number | null;
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
@@ -62,6 +63,7 @@ export async function sendChat(state: ChatState): Promise<boolean> {
|
||||
const runId = generateUUID();
|
||||
state.chatRunId = runId;
|
||||
state.chatStream = "";
|
||||
state.chatStreamStartedAt = now;
|
||||
try {
|
||||
await state.client.request("chat.send", {
|
||||
sessionKey: state.sessionKey,
|
||||
@@ -74,6 +76,7 @@ export async function sendChat(state: ChatState): Promise<boolean> {
|
||||
const error = String(err);
|
||||
state.chatRunId = null;
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatMessage = msg;
|
||||
state.lastError = error;
|
||||
state.chatMessages = [
|
||||
@@ -100,13 +103,25 @@ export function handleChatEvent(
|
||||
return null;
|
||||
|
||||
if (payload.state === "delta") {
|
||||
state.chatStream = extractText(payload.message) ?? state.chatStream;
|
||||
const next = extractText(payload.message);
|
||||
if (typeof next === "string") {
|
||||
const current = state.chatStream ?? "";
|
||||
if (!current || next.length >= current.length) {
|
||||
state.chatStream = next;
|
||||
}
|
||||
}
|
||||
} else if (payload.state === "final") {
|
||||
state.chatStream = null;
|
||||
state.chatRunId = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
} else if (payload.state === "aborted") {
|
||||
state.chatStream = null;
|
||||
state.chatRunId = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
} else if (payload.state === "error") {
|
||||
state.chatStream = null;
|
||||
state.chatRunId = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.lastError = payload.errorMessage ?? "chat error";
|
||||
}
|
||||
return payload.state;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
import type { SessionsListResult } from "../types";
|
||||
@@ -12,7 +13,9 @@ export type ChatProps = {
|
||||
loading: boolean;
|
||||
sending: boolean;
|
||||
messages: unknown[];
|
||||
toolMessages: unknown[];
|
||||
stream: string | null;
|
||||
streamStartedAt: number | null;
|
||||
draft: string;
|
||||
connected: boolean;
|
||||
canSend: boolean;
|
||||
@@ -77,19 +80,20 @@ export function renderChat(props: ChatProps) {
|
||||
|
||||
<div class="chat-thread" role="log" aria-live="polite">
|
||||
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
|
||||
${props.messages.map((m) => renderMessage(m))}
|
||||
${props.stream !== null
|
||||
? props.stream.trim().length > 0
|
||||
? renderMessage(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: props.stream }],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{ streaming: true },
|
||||
)
|
||||
: renderReadingIndicator()
|
||||
: nothing}
|
||||
${repeat(buildChatItems(props), (item) => item.key, (item) => {
|
||||
if (item.kind === "reading-indicator") return renderReadingIndicator();
|
||||
if (item.kind === "stream") {
|
||||
return renderMessage(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: item.text }],
|
||||
timestamp: item.startedAt,
|
||||
},
|
||||
{ streaming: true },
|
||||
);
|
||||
}
|
||||
return renderMessage(item.message);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="chat-compose">
|
||||
@@ -123,6 +127,76 @@ export function renderChat(props: ChatProps) {
|
||||
`;
|
||||
}
|
||||
|
||||
type ChatItem =
|
||||
| { kind: "message"; key: string; message: unknown }
|
||||
| { kind: "stream"; key: string; text: string; startedAt: number }
|
||||
| { kind: "reading-indicator"; key: string };
|
||||
|
||||
function buildChatItems(props: ChatProps): ChatItem[] {
|
||||
const items: ChatItem[] = [];
|
||||
const history = Array.isArray(props.messages) ? props.messages : [];
|
||||
const tools = Array.isArray(props.toolMessages) ? props.toolMessages : [];
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
items.push({ kind: "message", key: messageKey(history[i], i), message: history[i] });
|
||||
}
|
||||
for (let i = 0; i < tools.length; i++) {
|
||||
items.push({
|
||||
kind: "message",
|
||||
key: messageKey(tools[i], i + history.length),
|
||||
message: tools[i],
|
||||
});
|
||||
}
|
||||
|
||||
if (props.stream !== null) {
|
||||
const key = `stream:${props.sessionKey}:${props.streamStartedAt ?? "live"}`;
|
||||
if (props.stream.trim().length > 0) {
|
||||
items.push({
|
||||
kind: "stream",
|
||||
key,
|
||||
text: props.stream,
|
||||
startedAt: props.streamStartedAt ?? Date.now(),
|
||||
});
|
||||
} else {
|
||||
items.push({ kind: "reading-indicator", key });
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function messageKey(message: unknown, index: number): string {
|
||||
const m = message as Record<string, unknown>;
|
||||
const toolCallId = typeof m.toolCallId === "string" ? m.toolCallId : "";
|
||||
if (toolCallId) return `tool:${toolCallId}`;
|
||||
const id = typeof m.id === "string" ? m.id : "";
|
||||
if (id) return `msg:${id}`;
|
||||
const messageId = typeof m.messageId === "string" ? m.messageId : "";
|
||||
if (messageId) return `msg:${messageId}`;
|
||||
const timestamp = typeof m.timestamp === "number" ? m.timestamp : null;
|
||||
const role = typeof m.role === "string" ? m.role : "unknown";
|
||||
const fingerprint = extractText(message) ?? (typeof m.content === "string" ? m.content : null);
|
||||
const seed = fingerprint ?? safeJson(message) ?? String(index);
|
||||
const hash = fnv1a(seed);
|
||||
return timestamp ? `msg:${role}:${timestamp}:${hash}` : `msg:${role}:${hash}`;
|
||||
}
|
||||
|
||||
function safeJson(value: unknown): string | null {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fnv1a(input: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
hash ^= input.charCodeAt(i);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
||||
|
||||
type SessionOption = {
|
||||
key: string;
|
||||
updatedAt?: number | null;
|
||||
|
||||
@@ -17,6 +17,9 @@ export default defineConfig(({ command }) => {
|
||||
const base = envBase ? normalizeBase(envBase) : "/";
|
||||
return {
|
||||
base,
|
||||
optimizeDeps: {
|
||||
include: ["lit/directives/repeat.js"],
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(here, "../dist/control-ui"),
|
||||
emptyOutDir: true,
|
||||
|
||||
Reference in New Issue
Block a user