@k1e1n04/mav 0.1.18 → 0.1.19

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.
@@ -1,59 +1,52 @@
1
- import blessed from 'neo-blessed'
2
- import type { Widgets } from 'neo-blessed'
3
1
  import type { SessionManager } from '../session-manager.js'
4
2
  import type { AgentSession } from '../agent.js'
5
3
  import { getAgentDefaults, resolveSessionArgs } from '../agent-launch.js'
6
4
  import { completePath } from './path-completion.js'
5
+ import type { KeyInfo, TerminalUI } from './terminal.js'
6
+
7
+ type PromptState =
8
+ | { mode: 'agent'; selectedIndex: number }
9
+ | { mode: 'cwd'; agentType: string; value: string; candidates: string[] }
10
+ | { mode: 'error'; message: string }
11
+ | null
7
12
 
8
13
  export class OverviewUI {
14
+ private static readonly ANSI = {
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ dim: '\x1b[2m',
18
+ fgSlate: '\x1b[38;5;245m',
19
+ fgGreen: '\x1b[38;5;42m',
20
+ fgAmber: '\x1b[38;5;221m',
21
+ fgCyan: '\x1b[38;5;81m',
22
+ fgRed: '\x1b[38;5;203m',
23
+ } as const
24
+
9
25
  private static readonly STATUS_GROUPS = [
10
26
  { status: 'running', label: 'Working' },
11
27
  { status: 'idle', label: 'Waiting' },
12
28
  { status: 'done', label: 'Complete' },
13
29
  { status: 'error', label: 'Failed' },
14
30
  ] as const
15
- private static readonly STATUS_COLORS = {
16
- running: 'cyan',
17
- idle: 'yellow',
18
- done: 'green',
19
- error: 'red',
20
- } as const
21
31
 
22
- private screen: Widgets.Screen
32
+ private static readonly AGENT_TYPES = ['claude-code', 'codex', 'gemini-cli', 'copilot'] as const
33
+
34
+ private terminal: TerminalUI
23
35
  private manager: SessionManager
24
36
  private onSessionCreated?: (session: SessionManager['selectedSession']) => void
25
- private listBox: Widgets.ListElement
26
- private promptOpen = false
37
+ private promptState: PromptState = null
38
+ private displaySessionIds: string[] = []
39
+ private visible = false
27
40
 
28
41
  constructor(
29
- screen: Widgets.Screen,
42
+ terminal: TerminalUI,
30
43
  manager: SessionManager,
31
44
  onSessionCreated?: (session: SessionManager['selectedSession']) => void
32
45
  ) {
33
- this.screen = screen
46
+ this.terminal = terminal
34
47
  this.manager = manager
35
48
  this.onSessionCreated = onSessionCreated
36
49
 
37
- this.listBox = blessed.list({
38
- parent: screen,
39
- top: 0,
40
- left: 0,
41
- width: '100%',
42
- height: '100%',
43
- border: { type: 'line' },
44
- label: ' AGENTS ',
45
- tags: true,
46
- style: {
47
- selected: { bg: 'blue', fg: 'white' },
48
- border: { fg: 'cyan' },
49
- },
50
- // keys: true を設定すると blessed 組み込みの up()/down() もカスタムハンドラーと
51
- // 同時に実行され、selected がヘッダー行(cwd名やステータスグループ名)に一時的に
52
- // 移動してしまう競合が起きる。矢印キーはカスタムハンドラーのみで処理する。
53
- mouse: true,
54
- })
55
-
56
- this.bindKeys()
57
50
  this.syncList()
58
51
 
59
52
  manager.on('data', (sessionId: string) => {
@@ -61,184 +54,152 @@ export class OverviewUI {
61
54
  return
62
55
  }
63
56
  this.syncList()
64
- screen.render()
57
+ this.render()
65
58
  })
66
59
 
67
60
  manager.on('exit', () => {
68
61
  this.syncList()
69
- screen.render()
62
+ this.render()
70
63
  })
71
64
 
72
65
  manager.on('status', () => {
73
66
  this.syncList()
74
- screen.render()
67
+ this.render()
75
68
  })
76
69
 
77
70
  manager.on('name', () => {
78
71
  this.syncList()
79
- screen.render()
72
+ this.render()
80
73
  })
81
74
 
82
- // removeSession() は emitSelection() を呼ぶが、OverviewUI は 'selection' を
83
- // listen していないため、index.ts の exit ハンドラ経由で自動削除された場合に
84
- // UI が再描画されない。'selection' を listen して確実に同期する。
85
75
  manager.on('selection', () => {
86
76
  this.syncList()
87
- screen.render()
77
+ this.render()
88
78
  })
89
79
  }
90
80
 
91
- private bindKeys(): void {
92
- this.listBox.key(['up', 'k'], () => {
93
- if (this.manager.sessions.length === 0) return
81
+ handleKeypress(str: string, key: KeyInfo): void {
82
+ if (this.promptState?.mode === 'agent') {
83
+ this.handleAgentPromptKeypress(key)
84
+ return
85
+ }
86
+ if (this.promptState?.mode === 'cwd') {
87
+ this.handleCwdPromptKeypress(key)
88
+ return
89
+ }
90
+ if (this.promptState?.mode === 'error') {
91
+ if (key.name === 'enter' || key.name === 'return' || key.name === 'escape' || str === 'q') {
92
+ this.promptState = null
93
+ this.render()
94
+ }
95
+ return
96
+ }
97
+
98
+ if ((key.name === 'up' || str === 'k') && this.manager.sessions.length > 0) {
94
99
  this.moveSelection(-1)
95
- // syncList() と screen.render() は moveSelection() → selectSession() →
96
- // emitSelection() → 'selection' イベント経由で自動的に呼ばれる
97
- })
100
+ return
101
+ }
98
102
 
99
- this.listBox.key(['down', 'j'], () => {
100
- if (this.manager.sessions.length === 0) return
103
+ if ((key.name === 'down' || str === 'j') && this.manager.sessions.length > 0) {
101
104
  this.moveSelection(1)
102
- // syncList() と screen.render() は moveSelection() → selectSession() →
103
- // emitSelection() → 'selection' イベント経由で自動的に呼ばれる
104
- })
105
+ return
106
+ }
105
107
 
106
- this.listBox.key('n', () => {
108
+ if (str === 'n') {
107
109
  this.showAddPrompt()
108
- })
110
+ return
111
+ }
109
112
 
110
- this.listBox.key(['d', 'C-x'], () => {
113
+ if (str === 'd' || (key.ctrl && key.name === 'x')) {
111
114
  const session = this.manager.selectedSession
112
115
  if (session) {
113
116
  this.manager.removeSession(session.id)
114
117
  this.syncList()
115
- this.screen.render()
118
+ this.render()
116
119
  }
117
- })
120
+ }
118
121
  }
119
122
 
120
- private showAddPrompt(): void {
121
- if (this.promptOpen) return
122
- this.promptOpen = true
123
-
124
- const agentTypes = ['claude-code', 'codex', 'gemini-cli', 'copilot']
125
-
126
- const prompt = blessed.list({
127
- parent: this.screen,
128
- top: 'center',
129
- left: 'center',
130
- width: 40,
131
- height: agentTypes.length + 4,
132
- border: { type: 'line' },
133
- label: ' Select agent type ',
134
- items: agentTypes,
135
- keys: true,
136
- style: {
137
- selected: { bg: 'blue', fg: 'white' },
138
- border: { fg: 'green' },
139
- },
140
- })
123
+ private handleAgentPromptKeypress(key: KeyInfo): void {
124
+ const state = this.promptState
125
+ if (!state || state.mode !== 'agent') return
141
126
 
142
- const close = () => {
143
- this.promptOpen = false
144
- prompt.destroy()
145
- this.listBox.focus()
146
- this.screen.render()
127
+ if (key.name === 'escape') {
128
+ this.promptState = null
129
+ this.render()
130
+ return
147
131
  }
148
132
 
149
- prompt.key('enter', () => {
150
- const selectedIdx = (prompt as unknown as { selected: number }).selected ?? 0
151
- const selected = agentTypes[selectedIdx]!
152
- prompt.destroy()
153
- this.screen.render()
154
- this.showCwdPrompt(selected)
155
- })
133
+ if (key.name === 'up') {
134
+ state.selectedIndex =
135
+ (state.selectedIndex - 1 + OverviewUI.AGENT_TYPES.length) % OverviewUI.AGENT_TYPES.length
136
+ this.render()
137
+ return
138
+ }
156
139
 
157
- prompt.key('escape', close)
140
+ if (key.name === 'down') {
141
+ state.selectedIndex = (state.selectedIndex + 1) % OverviewUI.AGENT_TYPES.length
142
+ this.render()
143
+ return
144
+ }
158
145
 
159
- prompt.focus()
160
- this.screen.render()
146
+ if (key.name === 'enter' || key.name === 'return') {
147
+ const agentType = OverviewUI.AGENT_TYPES[state.selectedIndex]!
148
+ this.promptState = {
149
+ mode: 'cwd',
150
+ agentType,
151
+ value: process.cwd(),
152
+ candidates: [],
153
+ }
154
+ this.render()
155
+ }
161
156
  }
162
157
 
163
- private showCwdPrompt(agentType: string): void {
164
- let value = process.cwd()
165
- let candidateList: Widgets.ListElement | null = null
166
-
167
- const box = blessed.box({
168
- parent: this.screen,
169
- top: 'center',
170
- left: 'center',
171
- width: 60,
172
- height: 3,
173
- border: { type: 'line' },
174
- label: ' cwd Tab:補完 Enter:確定 Esc:キャンセル ',
175
- style: { border: { fg: 'green' } },
176
- })
158
+ private handleCwdPromptKeypress(key: KeyInfo): void {
159
+ const state = this.promptState
160
+ if (!state || state.mode !== 'cwd') return
177
161
 
178
- const renderInput = () => {
179
- box.setContent(value)
180
- this.screen.render()
162
+ if (key.name === 'escape') {
163
+ this.promptState = null
164
+ this.render()
165
+ return
181
166
  }
182
167
 
183
- const closeCandidates = () => {
184
- if (candidateList) {
185
- candidateList.destroy()
186
- candidateList = null
187
- }
168
+ if (key.name === 'enter' || key.name === 'return') {
169
+ const expanded = state.value.trim().replace(/^~/, process.env.HOME ?? '~') || process.cwd()
170
+ const agentType = state.agentType
171
+ this.promptState = null
172
+ this.render()
173
+ this.spawnAgent(agentType, expanded)
174
+ return
188
175
  }
189
176
 
190
- const cleanup = () => {
191
- this.screen.removeListener('keypress', onKeypress)
192
- closeCandidates()
193
- this.promptOpen = false
194
- box.destroy()
195
- this.listBox.focus()
196
- this.screen.render()
177
+ if (key.name === 'tab') {
178
+ const { completed, candidates } = completePath(state.value)
179
+ state.value = completed
180
+ state.candidates = candidates
181
+ this.render()
182
+ return
197
183
  }
198
184
 
199
- const onKeypress = (_ch: string, key: { name: string; ctrl?: boolean; meta?: boolean; sequence?: string }) => {
200
- if (!key) return
201
- closeCandidates()
202
-
203
- if (key.name === 'enter' || key.name === 'return') {
204
- const expanded = value.trim().replace(/^~/, process.env.HOME ?? '~') || process.cwd()
205
- cleanup()
206
- this.spawnAgent(agentType, expanded)
207
- } else if (key.name === 'escape') {
208
- cleanup()
209
- } else if (key.name === 'tab') {
210
- const { completed, candidates } = completePath(value)
211
- value = completed
212
- if (candidates.length > 1) {
213
- const maxVisible = Math.min(candidates.length, 8)
214
- candidateList = blessed.list({
215
- parent: this.screen,
216
- top: '50%',
217
- left: 'center',
218
- width: 60,
219
- height: maxVisible + 2,
220
- border: { type: 'line' },
221
- items: candidates,
222
- keys: false,
223
- style: { border: { fg: 'cyan' } },
224
- }) as Widgets.ListElement
225
- }
226
- renderInput()
227
- } else if (key.name === 'backspace') {
228
- value = value.slice(0, -1)
229
- renderInput()
230
- } else if (!key.ctrl && !key.meta && key.sequence?.length === 1) {
231
- value += key.sequence
232
- renderInput()
233
- }
185
+ state.candidates = []
186
+
187
+ if (key.name === 'backspace') {
188
+ state.value = state.value.slice(0, -1)
189
+ this.render()
190
+ return
234
191
  }
235
192
 
236
- // 現在のキーイベント(enter)がこのリスナーに届かないよう1tick遅らせる
237
- setImmediate(() => {
238
- this.screen.on('keypress', onKeypress)
239
- })
240
- box.focus()
241
- renderInput()
193
+ if (!key.ctrl && !key.meta && key.sequence?.length === 1) {
194
+ state.value += key.sequence
195
+ this.render()
196
+ }
197
+ }
198
+
199
+ private showAddPrompt(): void {
200
+ if (this.promptState) return
201
+ this.promptState = { mode: 'agent', selectedIndex: 0 }
202
+ this.render()
242
203
  }
243
204
 
244
205
  private spawnAgent(agentType: string, cwd: string): void {
@@ -268,66 +229,13 @@ export class OverviewUI {
268
229
  }
269
230
 
270
231
  private showError(message: string): void {
271
- const overlay = blessed.box({
272
- parent: this.screen,
273
- top: 'center',
274
- left: 'center',
275
- width: 50,
276
- height: message.split('\n').length + 4,
277
- border: { type: 'line' },
278
- label: ' Error ',
279
- content: `\n ${message.split('\n').join('\n ')}`,
280
- style: { border: { fg: 'red' }, label: { fg: 'red' } },
281
- keys: true,
282
- mouse: true,
283
- })
284
- overlay.key(['enter', 'escape', 'q'], () => {
285
- overlay.destroy()
286
- this.listBox.focus()
287
- this.screen.render()
288
- })
289
- overlay.focus()
290
- this.screen.render()
232
+ this.promptState = { mode: 'error', message }
233
+ this.render()
291
234
  }
292
235
 
293
236
  private syncList(): void {
294
237
  const orderedSessions = this.getOrderedSessions()
295
-
296
- const items: string[] = []
297
- const selectedId = this.manager.selectedSession?.id
298
- let selectedDisplayIndex = -1
299
-
300
- const cwdGroups = this.groupByCwd(orderedSessions)
301
- for (const [cwd, sessions] of cwdGroups) {
302
- items.push(` {bold}${this.shortenPath(cwd)}{/bold}`)
303
-
304
- for (const group of OverviewUI.STATUS_GROUPS) {
305
- const groupSessions = sessions.filter((s) => s.status === group.status)
306
- if (groupSessions.length === 0) continue
307
-
308
- items.push(` ${group.label}`)
309
- for (const session of groupSessions) {
310
- const statusIcon =
311
- session.status === 'running' ? '⣾'
312
- : session.status === 'idle' ? '○'
313
- : session.status === 'done' ? '✓'
314
- : '✗'
315
- const sessionTitle = session.displayName ?? session.id
316
- const sessionLabel = session.type ? `${sessionTitle} (${session.type})` : sessionTitle
317
- const content = `${statusIcon} ${sessionLabel} ${this.getStatusLabel(session.status)}`
318
- const color = OverviewUI.STATUS_COLORS[session.status]
319
- items.push(` {${color}-fg}${content}{/${color}-fg}`)
320
- if (session.id === selectedId) {
321
- selectedDisplayIndex = items.length - 1
322
- }
323
- }
324
- }
325
- }
326
-
327
- this.listBox.setItems(items)
328
- if (selectedDisplayIndex >= 0) {
329
- this.listBox.select(selectedDisplayIndex)
330
- }
238
+ this.displaySessionIds = orderedSessions.map((session) => session.id)
331
239
  }
332
240
 
333
241
  private groupByCwd(sessions: SessionManager['sessions']): Map<string, SessionManager['sessions']> {
@@ -361,18 +269,17 @@ export class OverviewUI {
361
269
  }
362
270
 
363
271
  private moveSelection(direction: -1 | 1): void {
364
- const orderedSessions = this.getOrderedSessions()
365
272
  const selectedId = this.manager.selectedSession?.id
366
- const currentIndex = selectedId
367
- ? orderedSessions.findIndex((session) => session.id === selectedId)
368
- : -1
273
+ if (this.displaySessionIds.length === 0) return
274
+
275
+ const currentIndex = selectedId ? this.displaySessionIds.indexOf(selectedId) : -1
369
276
  const nextIndex = currentIndex === -1
370
277
  ? 0
371
- : Math.max(0, Math.min(orderedSessions.length - 1, currentIndex + direction))
372
- const nextSession = orderedSessions[nextIndex]
373
- if (!nextSession) return
278
+ : (currentIndex + direction + this.displaySessionIds.length) % this.displaySessionIds.length
279
+ const nextSessionId = this.displaySessionIds[nextIndex]
280
+ if (!nextSessionId) return
374
281
 
375
- const managerIndex = this.manager.sessions.findIndex((session) => session.id === nextSession.id)
282
+ const managerIndex = this.manager.sessions.findIndex((session) => session.id === nextSessionId)
376
283
  if (managerIndex >= 0) {
377
284
  this.manager.selectSession(managerIndex)
378
285
  }
@@ -408,22 +315,174 @@ export class OverviewUI {
408
315
  }
409
316
  }
410
317
 
318
+ private countByStatus(status: string): number {
319
+ return this.manager.sessions.filter((session) => session.status === status).length
320
+ }
321
+
322
+ private stripAnsi(value: string): string {
323
+ return value.replace(/\x1b\[[0-9;]*m/g, '')
324
+ }
325
+
326
+ private padRight(value: string, width: number): string {
327
+ const visibleLength = this.stripAnsi(value).length
328
+ if (visibleLength >= width) {
329
+ return value
330
+ }
331
+ return value + ' '.repeat(width - visibleLength)
332
+ }
333
+
334
+ private color(text: string, ...codes: string[]): string {
335
+ return `${codes.join('')}${text}${OverviewUI.ANSI.reset}`
336
+ }
337
+
338
+ private getStatusColor(status: string): string {
339
+ switch (status) {
340
+ case 'running':
341
+ return OverviewUI.ANSI.fgGreen
342
+ case 'idle':
343
+ return OverviewUI.ANSI.fgAmber
344
+ case 'done':
345
+ return OverviewUI.ANSI.fgCyan
346
+ case 'error':
347
+ return OverviewUI.ANSI.fgRed
348
+ default:
349
+ return OverviewUI.ANSI.fgSlate
350
+ }
351
+ }
352
+
353
+ private makeRule(label: string, width = 64): string {
354
+ const text = ` ${label} `
355
+ if (text.length >= width) {
356
+ return text
357
+ }
358
+ const fill = '─'.repeat(width - text.length)
359
+ return text + fill
360
+ }
361
+
362
+ private formatSummary(): string {
363
+ const parts = OverviewUI.STATUS_GROUPS.map((group) => {
364
+ const count = this.countByStatus(group.status)
365
+ return this.color(`${group.label} ${count}`, OverviewUI.ANSI.bold, this.getStatusColor(group.status))
366
+ })
367
+ return parts.join(' · ')
368
+ }
369
+
370
+ private buildMainLines(): string[] {
371
+ const orderedSessions = this.getOrderedSessions()
372
+ const selectedId = this.manager.selectedSession?.id
373
+ const lines = [
374
+ '╭────────────────────────────── mav overview ──────────────────────────────╮',
375
+ `│ ${this.padRight('AGENTS', 74)} │`,
376
+ `│ ${this.padRight(this.formatSummary(), 74)} │`,
377
+ '╰──────────────────────────────────────────────────────────────────────────╯',
378
+ '',
379
+ ]
380
+
381
+ const cwdGroups = this.groupByCwd(orderedSessions)
382
+ for (const [cwd, sessions] of cwdGroups) {
383
+ lines.push(this.color(this.makeRule(this.shortenPath(cwd)), OverviewUI.ANSI.dim))
384
+
385
+ for (const group of OverviewUI.STATUS_GROUPS) {
386
+ const groupSessions = sessions.filter((s) => s.status === group.status)
387
+ if (groupSessions.length === 0) continue
388
+
389
+ lines.push(this.color(` ${group.label} (${groupSessions.length})`, OverviewUI.ANSI.bold, this.getStatusColor(group.status)))
390
+ for (const session of groupSessions) {
391
+ const color = this.getStatusColor(session.status)
392
+ const statusIcon =
393
+ session.status === 'running' ? '⣾'
394
+ : session.status === 'idle' ? '○'
395
+ : session.status === 'done' ? '✓'
396
+ : '✗'
397
+ const sessionTitle = session.displayName ?? session.id
398
+ const sessionLabel = session.type ? `${sessionTitle} (${session.type})` : sessionTitle
399
+ const prefix = session.id === selectedId ? '> ' : ' '
400
+ const sessionLine = `${prefix}${statusIcon} ${sessionLabel} ${this.getStatusLabel(session.status)}`
401
+ lines.push(session.id === selectedId
402
+ ? this.color(sessionLine, OverviewUI.ANSI.bold, color)
403
+ : this.color(sessionLine, color))
404
+ }
405
+
406
+ lines.push('')
407
+ }
408
+ }
409
+
410
+ if (orderedSessions.length === 0) {
411
+ lines.push(this.makeRule('empty'))
412
+ lines.push(' No sessions. Press n to add one.')
413
+ lines.push('')
414
+ }
415
+
416
+ lines.push('╭──────────────────────────────── controls ────────────────────────────────╮')
417
+ lines.push(`│ ${this.padRight('↑/↓ or j/k move Enter detail Ctrl+] back n new d delete q quit', 74)} │`)
418
+ lines.push('╰──────────────────────────────────────────────────────────────────────────╯')
419
+ return lines
420
+ }
421
+
422
+ private buildPromptLines(): string[] {
423
+ const state = this.promptState
424
+ if (!state) return []
425
+
426
+ const lines = ['', '╭──────────────────────────────── prompt ──────────────────────────────────╮']
427
+ if (state.mode === 'agent') {
428
+ lines.push(`│ ${this.padRight('Select agent type', 74)} │`)
429
+ lines.push('│ │')
430
+ for (const [index, agentType] of OverviewUI.AGENT_TYPES.entries()) {
431
+ lines.push(`│ ${this.padRight(`${index === state.selectedIndex ? '> ' : ' '}${agentType}`, 74)} │`)
432
+ }
433
+ lines.push('│ │')
434
+ lines.push(`│ ${this.padRight('Enter: select Esc: cancel', 74)} │`)
435
+ lines.push('╰──────────────────────────────────────────────────────────────────────────╯')
436
+ return lines
437
+ }
438
+
439
+ if (state.mode === 'cwd') {
440
+ lines.push(`│ ${this.padRight(`cwd for ${state.agentType}`, 74)} │`)
441
+ lines.push(`│ ${this.padRight(state.value, 74)} │`)
442
+ if (state.candidates.length > 1) {
443
+ lines.push('│ │')
444
+ lines.push(`│ ${this.padRight('Candidates:', 74)} │`)
445
+ for (const candidate of state.candidates.slice(0, 8)) {
446
+ lines.push(`│ ${this.padRight(` ${candidate}`, 74)} │`)
447
+ }
448
+ }
449
+ lines.push('│ │')
450
+ lines.push(`│ ${this.padRight('Tab: complete Enter: confirm Esc: cancel', 74)} │`)
451
+ lines.push('╰──────────────────────────────────────────────────────────────────────────╯')
452
+ return lines
453
+ }
454
+
455
+ lines.push(`│ ${this.padRight('Error', 74)} │`)
456
+ for (const line of state.message.split('\n')) {
457
+ lines.push(`│ ${this.padRight(line, 74)} │`)
458
+ }
459
+ lines.push('│ │')
460
+ lines.push(`│ ${this.padRight('Enter/Esc/q: close', 74)} │`)
461
+ lines.push('╰──────────────────────────────────────────────────────────────────────────╯')
462
+ return lines
463
+ }
464
+
465
+ private render(): void {
466
+ if (!this.visible) return
467
+ this.terminal.render([...this.buildMainLines(), ...this.buildPromptLines()].join('\n'))
468
+ }
469
+
411
470
  show(): void {
412
- this.listBox.show()
413
- this.listBox.focus()
471
+ this.visible = true
414
472
  this.syncList()
415
- this.screen.render()
473
+ this.render()
416
474
  }
417
475
 
418
476
  isPromptOpen(): boolean {
419
- return this.promptOpen
477
+ return this.promptState != null
420
478
  }
421
479
 
422
480
  resizeSelectedSession(): void {
423
481
  this.syncList()
482
+ this.render()
424
483
  }
425
484
 
426
485
  hide(): void {
427
- this.listBox.hide()
486
+ this.visible = false
428
487
  }
429
488
  }