import { buildSessionContext, codingTools, createAgentSession, createExtensionRuntime, getMarkdownTheme, SessionManager, type AgentSession, type AgentSessionEvent, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ResourceLoader, } from "@mariozechner/pi-coding-agent"; import { type AssistantMessage, type Message, type ThinkingLevel as AiThinkingLevel } from "@mariozechner/pi-ai"; import { Container, Input, Markdown, truncateToWidth, visibleWidth, type Focusable, type KeybindingsManager, type OverlayHandle, type TUI, } from "@mariozechner/pi-tui"; const BTW_ENTRY_TYPE = "btw-thread-entry"; const BTW_RESET_TYPE = "btw-thread-reset"; const BTW_SYSTEM_PROMPT = [ "You are BTW, a side-channel assistant embedded in the user's coding agent.", "You have access to the main conversation context — use it to give informed answers.", "Help with focused questions, planning, and quick explorations.", "Be direct and practical.", ].join(" "); const BTW_SUMMARY_PROMPT = "Summarize this side conversation for handoff into the main conversation. Keep key decisions, findings, risks, and next actions. Output only the summary."; type SessionThinkingLevel = "off" | AiThinkingLevel; type BtwDetails = { question: string; answer: string; timestamp: number; provider: string; model: string; thinkingLevel: SessionThinkingLevel; usage?: AssistantMessage["usage"]; }; type BtwResetDetails = { timestamp: number; }; type OverlayRuntime = { handle?: OverlayHandle; refresh?: () => void; close?: () => void; finish?: () => void; setDraft?: (value: string) => void; closed?: boolean; }; type SideSessionRuntime = { session: AgentSession; modelKey: string; unsubscribe: () => void; }; type ToolCallInfo = { toolCallId: string; toolName: string; args: string; status: "running" | "done" | "error"; }; function stripDynamicSystemPromptFooter(systemPrompt: string): string { return systemPrompt .replace(/\nCurrent date and time:[^\n]*(?:\nCurrent working directory:[^\n]*)?$/u, "") .replace(/\nCurrent working directory:[^\n]*$/u, "") .trim(); } function createBtwResourceLoader(ctx: ExtensionContext, appendSystemPrompt: string[] = [BTW_SYSTEM_PROMPT]): ResourceLoader { const extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() }; const systemPrompt = stripDynamicSystemPromptFooter(ctx.getSystemPrompt()); return { getExtensions: () => extensionsResult, getSkills: () => ({ skills: [], diagnostics: [] }), getPrompts: () => ({ prompts: [], diagnostics: [] }), getThemes: () => ({ themes: [], diagnostics: [] }), getAgentsFiles: () => ({ agentsFiles: [] }), getSystemPrompt: () => systemPrompt, getAppendSystemPrompt: () => appendSystemPrompt, extendResources: () => {}, reload: async () => {}, }; } function extractText(parts: AssistantMessage["content"]): string { return parts .filter((part) => part.type === "text") .map((part) => part.text) .join("\n") .trim(); } function extractEventAssistantText(message: unknown): string { if (!message || typeof message !== "object") { return ""; } const maybeMessage = message as { role?: unknown; content?: unknown }; if (maybeMessage.role !== "assistant" || !Array.isArray(maybeMessage.content)) { return ""; } return maybeMessage.content .filter((part): part is { type: "text"; text: string } => { return !!part && typeof part === "object" && (part as { type?: unknown }).type === "text"; }) .map((part) => part.text) .join("\n") .trim(); } function getLastAssistantMessage(session: AgentSession): AssistantMessage | null { for (let i = session.state.messages.length - 1; i >= 0; i--) { const message = session.state.messages[i]; if (message.role === "assistant") { return message as AssistantMessage; } } return null; } function buildSeedMessages(ctx: ExtensionContext, thread: BtwDetails[]): Message[] { const seed: Message[] = []; try { const contextMessages = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId()).messages; seed.push(...contextMessages); } catch { // Ignore context seed failures and continue with an empty side thread. } for (const item of thread) { seed.push( { role: "user", content: [{ type: "text", text: item.question }], timestamp: item.timestamp, }, { role: "assistant", content: [{ type: "text", text: item.answer }], provider: item.provider, model: item.model, api: ctx.model?.api ?? "openai-responses", usage: item.usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, stopReason: "stop", timestamp: item.timestamp, }, ); } return seed; } function formatThread(thread: BtwDetails[]): string { return thread .map((item) => `User: ${item.question.trim()}\nAssistant: ${item.answer.trim()}`) .join("\n\n---\n\n"); } function notify(ctx: ExtensionContext | ExtensionCommandContext, message: string, level: "info" | "warning" | "error"): void { if (ctx.hasUI) { ctx.ui.notify(message, level); } } class BtwOverlay extends Container implements Focusable { private readonly input: Input; private readonly tui: TUI; private readonly theme: ExtensionContext["ui"]["theme"]; private readonly keybindings: KeybindingsManager; private readonly getTranscript: (width: number, theme: ExtensionContext["ui"]["theme"]) => string[]; private readonly getStatus: () => string; private readonly onSubmitCallback: (value: string) => void; private readonly onDismissCallback: () => void; private _focused = false; get focused(): boolean { return this._focused; } set focused(value: boolean) { this._focused = value; this.input.focused = value; } constructor( tui: TUI, theme: ExtensionContext["ui"]["theme"], keybindings: KeybindingsManager, getTranscript: (width: number, theme: ExtensionContext["ui"]["theme"]) => string[], getStatus: () => string, onSubmit: (value: string) => void, onDismiss: () => void, ) { super(); this.tui = tui; this.theme = theme; this.keybindings = keybindings; this.getTranscript = getTranscript; this.getStatus = getStatus; this.onSubmitCallback = onSubmit; this.onDismissCallback = onDismiss; this.input = new Input(); this.input.onSubmit = (value) => { this.onSubmitCallback(value); }; this.input.onEscape = () => { this.onDismissCallback(); }; } handleInput(data: string): void { if (this.keybindings.matches(data, "selectCancel")) { this.onDismissCallback(); return; } this.input.handleInput(data); } setDraft(value: string): void { this.input.setValue(value); this.tui.requestRender(); } getDraft(): string { return this.input.getValue(); } private frameLine(content: string, innerWidth: number): string { const truncated = truncateToWidth(content, innerWidth, ""); const padding = Math.max(0, innerWidth - visibleWidth(truncated)); return `${this.theme.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.theme.fg("borderMuted", "│")}`; } private borderLine(innerWidth: number, edge: "top" | "bottom"): string { const left = edge === "top" ? "┌" : "└"; const right = edge === "top" ? "┐" : "┘"; return this.theme.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`); } override render(width: number): string[] { const dialogWidth = Math.max(56, Math.min(width, Math.floor(width * 0.9))); const innerWidth = Math.max(40, dialogWidth - 2); const terminalRows = process.stdout.rows ?? 30; const dialogHeight = Math.max(16, Math.min(30, Math.floor(terminalRows * 0.75))); const chromeHeight = 7; const transcriptHeight = Math.max(6, dialogHeight - chromeHeight); // Markdown renders to innerWidth already — no manual wrapping needed const transcript = this.getTranscript(innerWidth, this.theme); const visibleTranscript = transcript.slice(-transcriptHeight); const transcriptPadding = Math.max(0, transcriptHeight - visibleTranscript.length); const status = this.getStatus(); const previousFocused = this.input.focused; this.input.focused = false; const inputLine = this.input.render(innerWidth)[0] ?? ""; this.input.focused = previousFocused; const lines = [ this.borderLine(innerWidth, "top"), this.frameLine(this.theme.fg("accent", this.theme.bold(" BTW side chat ")), innerWidth), this.frameLine(this.theme.fg("dim", "Separate side conversation. Esc closes."), innerWidth), this.theme.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`), ]; for (const line of visibleTranscript) { lines.push(this.frameLine(line, innerWidth)); } for (let i = 0; i < transcriptPadding; i++) { lines.push(this.frameLine("", innerWidth)); } lines.push(this.theme.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`)); lines.push(this.frameLine(this.theme.fg("warning", status), innerWidth)); lines.push( `${this.theme.fg("borderMuted", "│")}${inputLine}${this.theme.fg("borderMuted", "│")}`, ); lines.push(this.frameLine(this.theme.fg("dim", "Enter submit · Esc close"), innerWidth)); lines.push(this.borderLine(innerWidth, "bottom")); return lines; } } export default function (pi: ExtensionAPI) { let thread: BtwDetails[] = []; let pendingQuestion: string | null = null; let pendingAnswer = ""; let pendingError: string | null = null; let pendingToolCalls: ToolCallInfo[] = []; let sideBusy = false; let overlayStatus = "Ready"; let overlayDraft = ""; let overlayRuntime: OverlayRuntime | null = null; let activeSideSession: SideSessionRuntime | null = null; let overlayRefreshTimer: ReturnType | null = null; const mdTheme = getMarkdownTheme(); function getModelKey(ctx: ExtensionContext): string { const model = ctx.model; return model ? `${model.provider}/${model.id}` : "none"; } function renderMarkdownLines(text: string, width: number): string[] { if (!text) return []; try { const md = new Markdown(text, 0, 0, mdTheme); return md.render(width); } catch { // Fall back to plain text wrapping if Markdown rendering fails return text.split("\n").flatMap((line) => { if (!line) return [""]; const wrapped: string[] = []; for (let i = 0; i < line.length; i += width) { wrapped.push(line.slice(i, i + width)); } return wrapped.length > 0 ? wrapped : [""]; }); } } function formatToolArgs(toolName: string, args: unknown): string { if (!args || typeof args !== "object") return ""; const a = args as Record; switch (toolName) { case "bash": return typeof a.command === "string" ? truncateToWidth(a.command.split("\n")[0], 50, "…") : ""; case "read": case "write": case "edit": return typeof a.path === "string" ? a.path : ""; default: { const first = Object.values(a)[0]; return typeof first === "string" ? truncateToWidth(first.split("\n")[0], 40, "…") : ""; } } } function renderToolCallLines(toolCalls: ToolCallInfo[], theme: ExtensionContext["ui"]["theme"], width: number): string[] { const lines: string[] = []; for (const tc of toolCalls) { const icon = tc.status === "running" ? "⚙" : tc.status === "error" ? "✗" : "✓"; const color = tc.status === "error" ? "error" : tc.status === "done" ? "success" : "dim"; const label = theme.fg(color, `${icon} `) + theme.fg("toolTitle", tc.toolName); const argsText = tc.args ? theme.fg("dim", ` ${tc.args}`) : ""; lines.push(truncateToWidth(` ${label}${argsText}`, width, "")); } return lines; } function getTranscriptLines(width: number, theme: ExtensionContext["ui"]["theme"]): string[] { try { return getTranscriptLinesInner(width, theme); } catch (error) { return [theme.fg("error", `Render error: ${error instanceof Error ? error.message : String(error)}`)]; } } function getTranscriptLinesInner(width: number, theme: ExtensionContext["ui"]["theme"]): string[] { if (thread.length === 0 && !pendingQuestion && !pendingAnswer && !pendingError) { return [theme.fg("dim", "No BTW messages yet. Type a question below.")]; } const lines: string[] = []; for (const item of thread.slice(-6)) { // User message const userText = item.question.trim().split("\n")[0]; lines.push(theme.fg("accent", theme.bold("You: ")) + truncateToWidth(userText, width - 5, "…")); lines.push(""); // Assistant message rendered as markdown const mdLines = renderMarkdownLines(item.answer, width); lines.push(...mdLines); lines.push(""); } if (pendingQuestion) { const userText = pendingQuestion.trim().split("\n")[0]; lines.push(theme.fg("accent", theme.bold("You: ")) + truncateToWidth(userText, width - 5, "…")); // Show tool calls inline if (pendingToolCalls.length > 0) { lines.push(...renderToolCallLines(pendingToolCalls, theme, width)); } if (pendingError) { lines.push(theme.fg("error", `❌ ${pendingError}`)); } else if (pendingAnswer) { lines.push(""); const mdLines = renderMarkdownLines(pendingAnswer, width); lines.push(...mdLines); } else if (pendingToolCalls.length === 0) { lines.push(theme.fg("dim", "…")); } } // Trim trailing empty line while (lines.length > 0 && lines[lines.length - 1] === "") { lines.pop(); } return lines; } function syncOverlay(): void { overlayRuntime?.refresh?.(); } function scheduleOverlayRefresh(): void { if (overlayRefreshTimer) { return; } overlayRefreshTimer = setTimeout(() => { overlayRefreshTimer = null; syncOverlay(); }, 16); } function setOverlayStatus(status: string, throttled = false): void { overlayStatus = status; if (throttled) { scheduleOverlayRefresh(); } else { syncOverlay(); } } function dismissOverlay(): void { overlayRuntime?.close?.(); overlayRuntime = null; if (overlayRefreshTimer) { clearTimeout(overlayRefreshTimer); overlayRefreshTimer = null; } } function setOverlayDraft(value: string): void { overlayDraft = value; overlayRuntime?.setDraft?.(value); } async function disposeSideSession(): Promise { const current = activeSideSession; activeSideSession = null; if (!current) { return; } try { current.unsubscribe(); } catch { // Ignore unsubscribe errors during cleanup. } try { await current.session.abort(); } catch { // Ignore abort errors during cleanup. } current.session.dispose(); if (overlayRefreshTimer) { clearTimeout(overlayRefreshTimer); overlayRefreshTimer = null; } } async function resetThread(ctx: ExtensionContext | ExtensionCommandContext, persist = true): Promise { thread = []; pendingQuestion = null; pendingAnswer = ""; pendingError = null; pendingToolCalls = []; sideBusy = false; setOverlayDraft(""); setOverlayStatus("Ready"); await disposeSideSession(); if (persist) { const details: BtwResetDetails = { timestamp: Date.now() }; pi.appendEntry(BTW_RESET_TYPE, details); } syncOverlay(); } async function restoreThread(ctx: ExtensionContext): Promise { await disposeSideSession(); thread = []; pendingQuestion = null; pendingAnswer = ""; pendingError = null; pendingToolCalls = []; sideBusy = false; overlayStatus = "Ready"; overlayDraft = ""; const branch = ctx.sessionManager.getBranch(); let lastResetIndex = -1; for (let i = 0; i < branch.length; i++) { const entry = branch[i]; if (entry.type === "custom" && entry.customType === BTW_RESET_TYPE) { lastResetIndex = i; } } for (const entry of branch.slice(lastResetIndex + 1)) { if (entry.type !== "custom" || entry.customType !== BTW_ENTRY_TYPE) { continue; } const details = entry.data as BtwDetails | undefined; if (!details?.question || !details.answer) { continue; } thread.push(details); } syncOverlay(); } async function createSideSession(ctx: ExtensionCommandContext): Promise { if (!ctx.model) { return null; } const { session } = await createAgentSession({ sessionManager: SessionManager.inMemory(), model: ctx.model, modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"], thinkingLevel: pi.getThinkingLevel() as SessionThinkingLevel, tools: codingTools, resourceLoader: createBtwResourceLoader(ctx), }); const seedMessages = buildSeedMessages(ctx, thread); if (seedMessages.length > 0) { session.agent.replaceMessages(seedMessages as typeof session.state.messages); } const unsubscribe = session.subscribe((event: AgentSessionEvent) => { if (!sideBusy || !pendingQuestion) { return; } switch (event.type) { case "message_start": case "message_update": case "message_end": { const streamed = extractEventAssistantText(event.message); if (streamed) { pendingAnswer = streamed; pendingError = null; } setOverlayStatus(event.type === "message_end" ? "Finalizing side response..." : "Streaming side response...", true); return; } case "tool_execution_start": { const toolName = (event as { toolName?: string }).toolName ?? "unknown"; try { pendingToolCalls.push({ toolCallId: (event as { toolCallId?: string }).toolCallId ?? "", toolName, args: formatToolArgs(toolName, (event as { args?: unknown }).args), status: "running", }); } catch { // Ignore tool tracking failures } setOverlayStatus(`Running tool: ${toolName}...`, true); return; } case "tool_execution_end": { const endToolName = (event as { toolName?: string }).toolName ?? "unknown"; const tc = pendingToolCalls.find( (t) => t.toolName === endToolName && t.status === "running", ); if (tc) { tc.status = (event as { isError?: boolean }).isError ? "error" : "done"; } setOverlayStatus("Streaming side response...", true); return; } case "turn_end": { setOverlayStatus("Finalizing side response...", true); return; } default: return; } }); return { session, modelKey: getModelKey(ctx), unsubscribe, }; } async function ensureSideSession(ctx: ExtensionCommandContext): Promise { if (!ctx.model) { return null; } const expectedModelKey = getModelKey(ctx); if (activeSideSession && activeSideSession.modelKey === expectedModelKey) { return activeSideSession; } await disposeSideSession(); activeSideSession = await createSideSession(ctx); return activeSideSession; } async function ensureOverlay(ctx: ExtensionCommandContext | ExtensionContext): Promise { if (!ctx.hasUI) { return; } if (overlayRuntime?.handle) { overlayRuntime.handle.setHidden(false); overlayRuntime.handle.focus(); overlayRuntime.refresh?.(); return; } const runtime: OverlayRuntime = {}; const closeRuntime = () => { if (runtime.closed) { return; } runtime.closed = true; runtime.handle?.hide(); if (overlayRuntime === runtime) { overlayRuntime = null; } runtime.finish?.(); }; runtime.close = closeRuntime; overlayRuntime = runtime; void ctx.ui .custom( async (tui, theme, keybindings, done) => { runtime.finish = () => done(); const overlay = new BtwOverlay( tui, theme, keybindings, (width, t) => getTranscriptLines(width, t), () => overlayStatus, (value) => { void submitFromOverlay(ctx, value); }, () => { void closeOverlayFlow(ctx); }, ); overlay.focused = true; overlay.setDraft(overlayDraft); runtime.setDraft = (value) => overlay.setDraft(value); runtime.refresh = () => { overlay.focused = runtime.handle?.isFocused() ?? false; tui.requestRender(); }; runtime.close = () => { overlayDraft = overlay.getDraft(); closeRuntime(); }; if (runtime.closed) { done(); } return overlay; }, { overlay: true, overlayOptions: { width: "80%", minWidth: 72, maxHeight: "78%", anchor: "top-center", margin: { top: 1, left: 2, right: 2 }, }, onHandle: (handle) => { runtime.handle = handle; handle.focus(); if (runtime.closed) { closeRuntime(); } }, }, ) .catch((error) => { if (overlayRuntime === runtime) { overlayRuntime = null; } notify(ctx, error instanceof Error ? error.message : String(error), "error"); }); } async function summarizeThread(ctx: ExtensionContext, items: BtwDetails[]): Promise { const model = ctx.model; if (!model) { throw new Error("No active model selected."); } const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model); if (!auth.ok) { throw new Error(auth.error); } const { session } = await createAgentSession({ sessionManager: SessionManager.inMemory(), model, modelRegistry: ctx.modelRegistry as AgentSession["modelRegistry"], thinkingLevel: "off", tools: [], resourceLoader: createBtwResourceLoader(ctx, [BTW_SUMMARY_PROMPT]), }); try { await session.prompt(formatThread(items), { source: "extension" }); const response = getLastAssistantMessage(session); if (!response) { throw new Error("Summary finished without a response."); } if (response.stopReason === "aborted") { throw new Error("Summary request was aborted."); } if (response.stopReason === "error") { throw new Error(response.errorMessage || "Summary request failed."); } return extractText(response.content) || "(No summary generated)"; } finally { try { await session.abort(); } catch { // Ignore abort errors during temporary session teardown. } session.dispose(); } } async function injectSummaryIntoMain(ctx: ExtensionContext | ExtensionCommandContext): Promise { if (thread.length === 0) { notify(ctx, "No BTW thread to summarize.", "warning"); return; } setOverlayStatus("Summarizing BTW thread for injection..."); try { const summary = await summarizeThread(ctx, thread); const message = `Summary of my BTW side conversation:\n\n${summary}`; if (ctx.isIdle()) { pi.sendUserMessage(message); } else { pi.sendUserMessage(message, { deliverAs: "followUp" }); } await resetThread(ctx); notify(ctx, "Injected BTW summary into main chat.", "info"); } catch (error) { notify(ctx, error instanceof Error ? error.message : String(error), "error"); } } async function closeOverlayFlow(ctx: ExtensionContext | ExtensionCommandContext): Promise { dismissOverlay(); if (!ctx.hasUI) { return; } if (thread.length === 0) { return; } const choice = await ctx.ui.select("Close BTW:", ["Keep side thread", "Inject summary into main chat"]); if (choice === "Inject summary into main chat") { await injectSummaryIntoMain(ctx); } } async function runBtwPrompt(ctx: ExtensionCommandContext, question: string): Promise { const model = ctx.model; if (!model) { setOverlayStatus("No active model selected."); notify(ctx, "No active model selected.", "error"); return; } const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model); if (!auth.ok) { const message = auth.error; setOverlayStatus(message); notify(ctx, message, "error"); return; } if (sideBusy) { notify(ctx, "BTW is still processing the previous message.", "warning"); return; } const side = await ensureSideSession(ctx); if (!side) { notify(ctx, "Unable to create BTW side session.", "error"); return; } sideBusy = true; pendingQuestion = question; pendingAnswer = ""; pendingError = null; pendingToolCalls = []; setOverlayStatus("Streaming side response..."); syncOverlay(); try { await side.session.prompt(question, { source: "extension" }); const response = getLastAssistantMessage(side.session); if (!response) { throw new Error("BTW request finished without a response."); } if (response.stopReason === "aborted") { throw new Error("BTW request aborted."); } if (response.stopReason === "error") { throw new Error(response.errorMessage || "BTW request failed."); } const answer = extractText(response.content) || "(No text response)"; pendingAnswer = answer; const details: BtwDetails = { question, answer, timestamp: Date.now(), provider: model.provider, model: model.id, thinkingLevel: pi.getThinkingLevel() as SessionThinkingLevel, usage: response.usage, }; thread.push(details); pi.appendEntry(BTW_ENTRY_TYPE, details); pendingQuestion = null; pendingAnswer = ""; pendingToolCalls = []; setOverlayStatus("Ready for the next side question."); } catch (error) { const message = error instanceof Error ? error.message : String(error); pendingError = message; setOverlayStatus("BTW request failed."); notify(ctx, message, "error"); } finally { sideBusy = false; syncOverlay(); } } async function submitFromOverlay(ctx: ExtensionContext | ExtensionCommandContext, rawValue: string): Promise { const question = rawValue.trim(); if (!question) { setOverlayStatus("Enter a question first."); return; } setOverlayDraft(""); if (!("waitForIdle" in ctx)) { setOverlayStatus("BTW submit requires command context. Re-open with /btw."); return; } await runBtwPrompt(ctx, question); } pi.registerCommand("btw", { description: "Open a simple BTW side-chat popover. `/btw ` asks immediately, `/btw` opens the side thread.", handler: async (args, ctx) => { const question = args.trim(); if (!question) { if (thread.length > 0 && ctx.hasUI) { const choice = await ctx.ui.select("BTW side chat:", [ "Continue previous conversation", "Start fresh", ]); if (choice === "Continue previous conversation") { // Dispose session so it's recreated with fresh main context on next submit await disposeSideSession(); setOverlayStatus("Continuing BTW thread."); await ensureOverlay(ctx); } else if (choice === "Start fresh") { await resetThread(ctx, true); setOverlayStatus("Ready"); await ensureOverlay(ctx); } // null = user cancelled (Esc), do nothing } else { await resetThread(ctx, true); setOverlayStatus("Ready"); await ensureOverlay(ctx); } return; } await ensureOverlay(ctx); await runBtwPrompt(ctx, question); }, }); pi.on("session_start", async (_event, ctx) => { await restoreThread(ctx); }); pi.on("session_switch", async (_event, ctx) => { await restoreThread(ctx); }); pi.on("session_tree", async (_event, ctx) => { await restoreThread(ctx); }); pi.on("session_shutdown", async () => { await disposeSideSession(); dismissOverlay(); }); }