@underactive/pi-topping-moa-fusion 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -297,12 +297,20 @@ export class MoaProgressWidget implements MoaProgressView {
297
297
  }
298
298
 
299
299
  /**
300
- * Start the fan-out phase with the initial (working) proposer statuses.
300
+ * Start the fan-out phase, marking only the slots that fit the process-pool
301
+ * limit as working. The runner promotes queued slots when they acquire one.
301
302
  * `thinking[i]` tints proposer `i`'s meter; an absent entry falls to accent.
302
303
  */
303
- startFanout(refs: ModelRef[], thinking: (ThinkingLevel | undefined)[] = []): void {
304
+ startFanout(refs: ModelRef[], thinking: (ThinkingLevel | undefined)[] = [], maxConcurrency = refs.length): void {
304
305
  const startedAt = Date.now();
305
- this.statuses = refs.map((ref, index) => ({ ref, phase: "Plan" as const, state: "working" as const, startedAt, thinking: thinking[index] }));
306
+ const activeCount = Number.isFinite(maxConcurrency) ? Math.max(1, Math.floor(maxConcurrency)) : refs.length;
307
+ this.statuses = refs.map((ref, index) => ({
308
+ ref,
309
+ phase: "Plan" as const,
310
+ state: index < activeCount ? "working" as const : "queued" as const,
311
+ startedAt: index < activeCount ? startedAt : undefined,
312
+ thinking: thinking[index],
313
+ }));
306
314
  this.active = "Plan";
307
315
  this.mountTable();
308
316
  }
@@ -341,14 +349,17 @@ export class MoaProgressWidget implements MoaProgressView {
341
349
  const wasSettled = s.state === "done" || s.state === "error" || s.state === "cancelled";
342
350
  s.state = state;
343
351
  s.detail = detail;
344
- // Re-activation (a settled proposer being retried) un-freezes the clock
345
- // and must not show output from the failed attempt.
346
- if (state === "working" && wasSettled) {
352
+ // A settled proposer entering a queued retry/round must clear its preview
353
+ // before it can be promoted to working, so it never shows stale output.
354
+ if ((state === "working" || state === "queued") && wasSettled) {
347
355
  s.transcript = undefined;
348
356
  s.transcriptRevision = (s.transcriptRevision ?? 0) + 1;
349
357
  }
350
358
  if (!isActive(state)) s.endedAt ??= Date.now();
351
- else s.endedAt = undefined;
359
+ else {
360
+ s.startedAt ??= Date.now();
361
+ s.endedAt = undefined;
362
+ }
352
363
  }
353
364
 
354
365
  /** Update one proposer's live context, turn, tool-call, and cost readings (by index). */
@@ -386,6 +397,46 @@ export class MoaProgressWidget implements MoaProgressView {
386
397
  s.transcriptRevision = (s.transcriptRevision ?? 0) + 1;
387
398
  }
388
399
 
400
+ /**
401
+ * Point a settled proposer row at a replacement model and clear every reading
402
+ * from the abandoned attempt, so the row reads as freshly queued. The elapsed
403
+ * clock is left unstamped: `update(index, "working")` starts it when the slot
404
+ * actually runs. Remounts the table, which the replacement prompt stopped —
405
+ * `update()` alone clears only the transcript, leaving stale turns/tools/cost/
406
+ * context behind, and `stopWidget()` (needed so the select/picker are not
407
+ * fighting the sticky widget) unmounts it, so only `mountTable()` remounts.
408
+ */
409
+ replaceProposerModel(index: number, ref: ModelRef, thinking?: ThinkingLevel): void {
410
+ const s = this.statuses[index];
411
+ if (!s) return;
412
+ s.ref = ref;
413
+ s.phase = "Plan";
414
+ s.thinking = thinking;
415
+ s.state = "queued";
416
+ s.detail = undefined;
417
+ // Clear every reading from the abandoned attempt, matching the wasSettled
418
+ // branch of update(): the row must read as freshly queued, and the
419
+ // transcriptRevision bump invalidates the preview cache.
420
+ s.contextTokens = undefined;
421
+ s.activity = undefined;
422
+ s.activityHistory = [];
423
+ s.outputTokens = undefined;
424
+ s.outputRevision = undefined;
425
+ s.turns = undefined;
426
+ s.toolCalls = undefined;
427
+ s.costUsd = undefined;
428
+ s.transcript = undefined;
429
+ s.startedAt = undefined;
430
+ s.endedAt = undefined;
431
+ s.transcriptRevision = (s.transcriptRevision ?? 0) + 1;
432
+ this.mountTable();
433
+ }
434
+
435
+ /** Re-show the table after a prompt stopped it, without changing any row. Idempotent. */
436
+ resumeTable(): void {
437
+ this.mountTable();
438
+ }
439
+
389
440
  /**
390
441
  * Move a role row into `working`, starting (or keeping) its elapsed clock.
391
442
  * Each activation replaces the row's thinking level (with `undefined` when the
@@ -539,7 +590,7 @@ export class MoaProgressWidget implements MoaProgressView {
539
590
  private toRow(s: ProposerStatus, phase: MoaPhase, workingText: string, now: number, firstOfPhase: boolean): ProgressRow {
540
591
  const statusText =
541
592
  s.state === "working" ? (s.detail ?? workingText)
542
- : s.state === "cancelling" ? "cancelling…"
593
+ : s.state === "cancelling" ? "cancelling"
543
594
  : s.state === "cancelled" ? "cancelled"
544
595
  : s.state === "error" ? s.detail ?? "error"
545
596
  : s.state === "queued" ? "queued"
@@ -17,6 +17,10 @@ import { type Component, type TUI } from "@earendil-works/pi-tui";
17
17
  import {
18
18
  defaultThinkingForModel,
19
19
  loadMoaConfig,
20
+ MAX_CONCURRENT_AGENTS,
21
+ MAX_VERIFICATION_REPAIRS_LIMIT,
22
+ MIN_CONCURRENT_AGENTS,
23
+ MIN_VERIFICATION_REPAIRS,
20
24
  saveMoaConfig,
21
25
  type MoaConfig,
22
26
  } from "../config/settings.ts";
@@ -49,8 +53,6 @@ interface SetupSlot {
49
53
  description: string;
50
54
  /** Whether the overview row appends the selected thinking level. */
51
55
  showThinkingValue: boolean;
52
- /** Where a confirmed selection is persisted. */
53
- target: { kind: "agent"; agentName: string } | { kind: "role" };
54
56
  }
55
57
 
56
58
  const SLOTS: readonly SetupSlot[] = [
@@ -59,14 +61,12 @@ const SLOTS: readonly SetupSlot[] = [
59
61
  title: agent.title,
60
62
  description: agent.description,
61
63
  showThinkingValue: true,
62
- target: { kind: "agent", agentName: agent.name },
63
64
  })),
64
65
  {
65
66
  menuLabel: "cheap/fast agent",
66
67
  title: "Cheap / fast agent — plan-file naming",
67
68
  description: "Only summarizes a plan prompt into a short file name. Always called with thinking off and falls back safely if it cannot respond, so pick the cheapest model available.",
68
69
  showThinkingValue: false,
69
- target: { kind: "role" },
70
70
  },
71
71
  ];
72
72
 
@@ -77,6 +77,8 @@ interface SetupDraft {
77
77
  thinkingOverrides: Record<string, ThinkingLevel>;
78
78
  autoResolveConflicts: boolean;
79
79
  useSummaryName: boolean;
80
+ maxConcurrentAgents: number;
81
+ maxVerificationRepairs: number;
80
82
  rosters: PlanRoster[];
81
83
  }
82
84
 
@@ -90,6 +92,8 @@ class MoaSetupComponent implements Component {
90
92
  private readonly menu: MenuComponent;
91
93
  private autoResolveConflicts: boolean;
92
94
  private useSummaryName: boolean;
95
+ private maxConcurrentAgents: number;
96
+ private maxVerificationRepairs: number;
93
97
 
94
98
  constructor(
95
99
  private readonly tui: TUI,
@@ -105,6 +109,8 @@ class MoaSetupComponent implements Component {
105
109
  this.twoPane = new TwoPaneModelThinking(tui, theme, availableRefs, config, currentThinking, ctx);
106
110
  this.autoResolveConflicts = draft.autoResolveConflicts;
107
111
  this.useSummaryName = draft.useSummaryName;
112
+ this.maxConcurrentAgents = draft.maxConcurrentAgents;
113
+ this.maxVerificationRepairs = draft.maxVerificationRepairs;
108
114
  const slotItem = (slotIndex: number): MenuItem => ({
109
115
  id: `role-${slotIndex}`,
110
116
  label: SLOTS[slotIndex]!.menuLabel,
@@ -130,6 +136,28 @@ class MoaSetupComponent implements Component {
130
136
  { title: "options", items: [
131
137
  { id: "auto-resolve", label: "auto resolve conflicts", value: this.autoResolveConflicts, onChange: (value: boolean) => { this.autoResolveConflicts = value; } },
132
138
  { id: "use-summary-name", label: "summarize plan names", value: this.useSummaryName, onChange: (value: boolean) => { this.useSummaryName = value; } },
139
+ {
140
+ id: "max-concurrent-agents",
141
+ label: "max concurrent agents",
142
+ values: Array.from(
143
+ { length: MAX_CONCURRENT_AGENTS - MIN_CONCURRENT_AGENTS + 1 },
144
+ (_unused, index) => String(MIN_CONCURRENT_AGENTS + index),
145
+ ),
146
+ valueIndex: this.maxConcurrentAgents - MIN_CONCURRENT_AGENTS,
147
+ description: "Child agents running at once in MoA, opinion, debate and plan subagent fan-outs. Keep at 1 for local agents.",
148
+ onChange: (valueIndex: number) => { this.maxConcurrentAgents = MIN_CONCURRENT_AGENTS + valueIndex; },
149
+ },
150
+ {
151
+ id: "max-verification-repairs",
152
+ label: "max verification repairs",
153
+ values: Array.from(
154
+ { length: MAX_VERIFICATION_REPAIRS_LIMIT - MIN_VERIFICATION_REPAIRS + 1 },
155
+ (_unused, index) => String(MIN_VERIFICATION_REPAIRS + index),
156
+ ),
157
+ valueIndex: this.maxVerificationRepairs - MIN_VERIFICATION_REPAIRS,
158
+ description: "Verifier-driven repair rounds after implementation (0 disables automatic repairs).",
159
+ onChange: (valueIndex: number) => { this.maxVerificationRepairs = MIN_VERIFICATION_REPAIRS + valueIndex; },
160
+ },
133
161
  ] },
134
162
  ],
135
163
  buttons: [
@@ -168,6 +196,8 @@ class MoaSetupComponent implements Component {
168
196
  thinkingOverrides: { ...this.draft.thinkingOverrides },
169
197
  autoResolveConflicts: this.autoResolveConflicts,
170
198
  useSummaryName: this.useSummaryName,
199
+ maxConcurrentAgents: this.maxConcurrentAgents,
200
+ maxVerificationRepairs: this.maxVerificationRepairs,
171
201
  rosters: [...this.draft.rosters],
172
202
  };
173
203
  }
@@ -299,6 +329,8 @@ export async function showMoaSetup(
299
329
  thinkingOverrides: {},
300
330
  autoResolveConflicts: saved.autoResolveConflicts,
301
331
  useSummaryName: saved.useSummaryName,
332
+ maxConcurrentAgents: saved.maxConcurrentAgents,
333
+ maxVerificationRepairs: saved.maxVerificationRepairs,
302
334
  rosters: [...saved.rosters],
303
335
  };
304
336
 
@@ -325,6 +357,10 @@ export async function showMoaSetup(
325
357
  );
326
358
  if (result === undefined) return false;
327
359
  if (result.action === "rosters") {
360
+ draft.autoResolveConflicts = result.draft.autoResolveConflicts;
361
+ draft.useSummaryName = result.draft.useSummaryName;
362
+ draft.maxConcurrentAgents = result.draft.maxConcurrentAgents;
363
+ draft.maxVerificationRepairs = result.draft.maxVerificationRepairs;
328
364
  draft.rosters = await showRosterManager(ctx, draft.rosters, { currentThinking });
329
365
  continue;
330
366
  }
@@ -360,6 +396,8 @@ export async function showMoaSetup(
360
396
  agentDefaultsConfigured: true,
361
397
  autoResolveConflicts: finalDraft.autoResolveConflicts,
362
398
  useSummaryName: finalDraft.useSummaryName,
399
+ maxConcurrentAgents: finalDraft.maxConcurrentAgents,
400
+ maxVerificationRepairs: finalDraft.maxVerificationRepairs,
363
401
  cheap: finalDraft.selections[AGENT_SLOT_COUNT],
364
402
  rosters: finalDraft.rosters,
365
403
  thinkingOverrides: { ...current.thinkingOverrides, ...finalDraft.thinkingOverrides },
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Working overlay — a small "please wait" card for blocking, non-agent work
3
+ * that has nowhere else to draw while it runs (e.g. summarizing a plan
4
+ * prompt into a name between the prompt editor closing and the next picker
5
+ * mounting). This is not the cancel-run surface: there is no agent table
6
+ * and no subagents to cancel. Esc abandons the wait rather than the flow
7
+ * that is waiting on it — the calling flow owns the overlay's lifetime
8
+ * through a session bridge, mirroring `CancelSession.closeOverlay`.
9
+ */
10
+
11
+ import type { ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
12
+ import { isKeyRelease, Key, matchesKey, visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
13
+ import { setTimeout as sleep } from "node:timers/promises";
14
+ import { formatElapsed } from "./agentStatus.ts";
15
+ import { BRAILLE_SPINNER_FRAMES, ROUNDED_SINGLE_BOX, UI_TICK_MS, createFrame } from "./chrome.ts";
16
+ import { shimmerString, type ShimmerTheme } from "./shimmer.ts";
17
+
18
+ const OVERLAY_MARGIN = 1;
19
+ /** Grace before mounting: work that settles inside this never flashes a card. */
20
+ const SHOW_DELAY_MS = 200;
21
+ /** Once shown, stay up at least this long so the card is readable, not a blink. */
22
+ const MIN_VISIBLE_MS = 500;
23
+
24
+ export interface WorkingOverlayOptions<T> {
25
+ /** Accent title row, e.g. "Naming the plan". */
26
+ title: string;
27
+ /** Dim, shimmering body line naming the work. */
28
+ detail: string;
29
+ /** Footer hint. Defaults to a plain wait line when `skip` is omitted. */
30
+ hint?: string;
31
+ /** Value to resolve with when the user presses esc. Omit to make the wait un-skippable. */
32
+ skip?: () => T;
33
+ }
34
+
35
+ /** Bridge an outside owner uses to close an overlay it does not render. Mirrors `CancelSession.closeOverlay`. */
36
+ export interface WorkingOverlaySession {
37
+ closeOverlay?: () => void;
38
+ /** Set when close was requested before the component mounted. */
39
+ closeRequested?: boolean;
40
+ }
41
+
42
+ /** Ask a (possibly not-yet-mounted) overlay to close. Safe to call before `showWorkingOverlay`'s factory has run. */
43
+ export function requestWorkingOverlayClose(session: WorkingOverlaySession): void {
44
+ session.closeRequested = true;
45
+ session.closeOverlay?.();
46
+ }
47
+
48
+ interface WorkingOverlayRenderOptions {
49
+ title: string;
50
+ detail: string;
51
+ hint?: string;
52
+ onSkip?: () => void;
53
+ }
54
+
55
+ /** Show the working overlay. Resolves when closed. Outside TUI mode there is no overlay surface. */
56
+ export async function showWorkingOverlay(
57
+ ctx: ExtensionContext,
58
+ options: WorkingOverlayRenderOptions,
59
+ session: WorkingOverlaySession,
60
+ ): Promise<void> {
61
+ if (ctx.mode !== "tui") return;
62
+ await ctx.ui.custom<void>(
63
+ (tui, theme, _keybindings, done) => new WorkingOverlayComponent(tui, theme, options, session, done),
64
+ {
65
+ overlay: true,
66
+ overlayOptions: {
67
+ anchor: "center",
68
+ width: "50%",
69
+ minWidth: 40,
70
+ margin: OVERLAY_MARGIN,
71
+ },
72
+ },
73
+ );
74
+ }
75
+
76
+ type Settled<T> = { ok: true; value: T } | { ok: false; error: unknown };
77
+
78
+ function unwrap<T>(settled: Settled<T>): T {
79
+ if (settled.ok) return settled.value;
80
+ throw settled.error;
81
+ }
82
+
83
+ /**
84
+ * Run `task` behind the working overlay. The overlay only mounts if `task`
85
+ * is still pending after `SHOW_DELAY_MS`, and once mounted stays up at
86
+ * least `MIN_VISIBLE_MS` so it never reads as a flicker. There is no
87
+ * automatic cutoff: a task that never settles keeps the card up until the
88
+ * user presses Esc (when `options.skip` is set) — deliberate, not an
89
+ * oversight, since the hint row is what turns that into a user choice
90
+ * rather than a hang.
91
+ */
92
+ export async function withWorkingOverlay<T>(
93
+ ctx: ExtensionContext,
94
+ options: WorkingOverlayOptions<T>,
95
+ task: (signal: AbortSignal) => Promise<T>,
96
+ ): Promise<T> {
97
+ const controller = new AbortController();
98
+ const work = task(controller.signal);
99
+ // No terminal surface: the caller still waits, but there is nothing to draw on.
100
+ if (ctx.mode !== "tui") return work;
101
+
102
+ // Reflect instead of letting `work` reject on its own timeline: it is raced
103
+ // against the grace timer and abandoned outright when the user skips, and an
104
+ // abandoned rejecting promise would otherwise surface as an unhandled rejection.
105
+ let pending = true;
106
+ const settled: Promise<Settled<T>> = work.then(
107
+ (value): Settled<T> => ({ ok: true, value }),
108
+ (error): Settled<T> => ({ ok: false, error }),
109
+ );
110
+ void settled.then(() => {
111
+ pending = false;
112
+ });
113
+
114
+ await Promise.race([settled, sleep(SHOW_DELAY_MS)]);
115
+ if (!pending) return unwrap(await settled);
116
+
117
+ const session: WorkingOverlaySession = {};
118
+ const shownAt = Date.now();
119
+ let skipped: { value: T } | undefined;
120
+ void settled.then(async () => {
121
+ if (skipped) return;
122
+ await sleep(Math.max(0, MIN_VISIBLE_MS - (Date.now() - shownAt)));
123
+ requestWorkingOverlayClose(session);
124
+ });
125
+
126
+ const skip = options.skip;
127
+ try {
128
+ await showWorkingOverlay(
129
+ ctx,
130
+ {
131
+ title: options.title,
132
+ detail: options.detail,
133
+ hint: options.hint,
134
+ // Esc skips: abort what we can, then leave the rest behind. The flow
135
+ // must move on even when a provider ignores the abort signal.
136
+ onSkip: skip
137
+ ? () => {
138
+ skipped = { value: skip() };
139
+ controller.abort();
140
+ }
141
+ : undefined,
142
+ },
143
+ session,
144
+ );
145
+ } catch {
146
+ // A stale ctx (e.g. after /reload) must not break the flow that is waiting.
147
+ }
148
+
149
+ if (skipped) return skipped.value;
150
+ return unwrap(await settled);
151
+ }
152
+
153
+ /** Copy for the plan/opinion/debate naming wait. No trailing ellipses — see CHANGELOG 0.2.0. */
154
+ export function planNamingOverlay(subject: "plan" | "opinion" | "debate", skip: () => string): WorkingOverlayOptions<string> {
155
+ return {
156
+ title: `Naming the ${subject}`,
157
+ detail: `summarizing your prompt into a short ${subject} name`,
158
+ hint: "esc skip naming · a name from your prompt is used instead",
159
+ skip,
160
+ };
161
+ }
162
+
163
+ /** The slice of pi's `Theme` this overlay needs. Structural so tests can stub it without `getFgAnsi`. */
164
+ interface WorkingOverlayTheme {
165
+ fg(color: ThemeColor, text: string): string;
166
+ bg(color: Parameters<Theme["bg"]>[0], text: string): string;
167
+ /** 24-bit ANSI escape for a color. Absent on test stubs, so the detail line falls back to flat text. */
168
+ getFgAnsi?(color: ThemeColor): string;
169
+ }
170
+
171
+ class WorkingOverlayComponent implements Component {
172
+ private readonly tui: TUI;
173
+ private readonly theme: WorkingOverlayTheme;
174
+ private readonly options: WorkingOverlayRenderOptions;
175
+ private readonly session: WorkingOverlaySession;
176
+ private readonly done: (value: void) => void;
177
+ private readonly startedAt: number;
178
+ private readonly timer: ReturnType<typeof setInterval>;
179
+ private spinFrame = 0;
180
+ private closed = false;
181
+
182
+ constructor(
183
+ tui: TUI,
184
+ theme: WorkingOverlayTheme,
185
+ options: WorkingOverlayRenderOptions,
186
+ session: WorkingOverlaySession,
187
+ done: (value: void) => void,
188
+ ) {
189
+ this.tui = tui;
190
+ this.theme = theme;
191
+ this.options = options;
192
+ this.session = session;
193
+ this.done = done;
194
+ this.startedAt = Date.now();
195
+ this.timer = setInterval(() => {
196
+ this.spinFrame = (this.spinFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
197
+ tui.requestRender();
198
+ }, UI_TICK_MS);
199
+ this.timer.unref();
200
+
201
+ session.closeOverlay = () => this.finish();
202
+ // The task can settle before `ctx.ui.custom` invokes this factory (only
203
+ // mounting triggers the factory). Defer so `done` is never called
204
+ // synchronously from inside the factory that constructs this component.
205
+ if (session.closeRequested) setTimeout(() => this.finish(), 0);
206
+ }
207
+
208
+ private finish(): void {
209
+ if (this.closed) return;
210
+ this.closed = true;
211
+ this.done(undefined);
212
+ }
213
+
214
+ handleInput(data: string): void {
215
+ // Under the Kitty keyboard protocol, the *release* of the key that
216
+ // closed the previous surface (e.g. Enter submitting the prompt editor)
217
+ // can land on this freshly mounted overlay a tick later. Only a press
218
+ // may skip. Same bug class as test/cancel-overlay-escape-release.test.mjs.
219
+ if (isKeyRelease(data)) return;
220
+ if (matchesKey(data, Key.escape)) {
221
+ if (this.options.onSkip) {
222
+ this.options.onSkip();
223
+ this.finish();
224
+ }
225
+ return;
226
+ }
227
+ // Everything else is swallowed: this is a blocking wait, not an
228
+ // interactive list, and stray keystrokes must not queue into whatever
229
+ // mounts next.
230
+ }
231
+
232
+ render(width: number): string[] {
233
+ const th = this.theme;
234
+ const innerWidth = Math.max(20, width - 4);
235
+ const frame = createFrame(th, innerWidth, {
236
+ glyphs: ROUNDED_SINGLE_BOX,
237
+ horizontalPadding: 1,
238
+ truncationMark: "…",
239
+ padToWidth: true,
240
+ minimumBodyWidth: 10,
241
+ });
242
+
243
+ const elapsed = formatElapsed(Date.now() - this.startedAt);
244
+ const titlePad = Math.max(1, frame.bodyWidth - visibleWidth(this.options.title) - visibleWidth(elapsed));
245
+ const titleRow = frame.row(`${th.fg("accent", this.options.title)}${" ".repeat(titlePad)}${th.fg("dim", elapsed)}`);
246
+
247
+ const spin = th.fg("accent", BRAILLE_SPINNER_FRAMES[this.spinFrame % BRAILLE_SPINNER_FRAMES.length]!);
248
+ const detailText = th.getFgAnsi
249
+ ? shimmerString(this.options.detail, Date.now() - this.startedAt, th as ShimmerTheme, "ltr", "normal")
250
+ : th.fg("dim", this.options.detail);
251
+ const detailRow = frame.row(`${spin} ${detailText}`);
252
+
253
+ const hint = this.options.hint ?? (this.options.onSkip ? "esc skip" : "please wait");
254
+ const hintRow = frame.row(th.fg("dim", hint));
255
+
256
+ return [frame.top(), titleRow, frame.separator(), detailRow, frame.separator(), hintRow, frame.bottom()];
257
+ }
258
+
259
+ invalidate(): void {}
260
+
261
+ dispose(): void {
262
+ clearInterval(this.timer);
263
+ if (this.session.closeOverlay) this.session.closeOverlay = undefined;
264
+ }
265
+ }