@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.
- package/dist/package.json +2 -5
- package/dist/src/ui/app.d.ts +4 -2
- package/dist/src/ui/app.js +32 -46
- package/dist/src/ui/app.js.map +1 -1
- package/dist/src/ui/detail.d.ts +5 -4
- package/dist/src/ui/detail.js +16 -18
- package/dist/src/ui/detail.js.map +1 -1
- package/dist/src/ui/overview.d.ts +21 -8
- package/dist/src/ui/overview.js +269 -233
- package/dist/src/ui/overview.js.map +1 -1
- package/dist/src/ui/terminal.d.ts +28 -0
- package/dist/src/ui/terminal.js +74 -0
- package/dist/src/ui/terminal.js.map +1 -0
- package/package.json +2 -5
- package/src/ui/app.ts +36 -46
- package/src/ui/detail.ts +19 -21
- package/src/ui/overview.ts +291 -232
- package/src/ui/terminal.ts +104 -0
- package/src/types/neo-blessed.d.ts +0 -5
package/src/ui/overview.ts
CHANGED
|
@@ -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
|
|
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
|
|
26
|
-
private
|
|
37
|
+
private promptState: PromptState = null
|
|
38
|
+
private displaySessionIds: string[] = []
|
|
39
|
+
private visible = false
|
|
27
40
|
|
|
28
41
|
constructor(
|
|
29
|
-
|
|
42
|
+
terminal: TerminalUI,
|
|
30
43
|
manager: SessionManager,
|
|
31
44
|
onSessionCreated?: (session: SessionManager['selectedSession']) => void
|
|
32
45
|
) {
|
|
33
|
-
this.
|
|
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
|
-
|
|
57
|
+
this.render()
|
|
65
58
|
})
|
|
66
59
|
|
|
67
60
|
manager.on('exit', () => {
|
|
68
61
|
this.syncList()
|
|
69
|
-
|
|
62
|
+
this.render()
|
|
70
63
|
})
|
|
71
64
|
|
|
72
65
|
manager.on('status', () => {
|
|
73
66
|
this.syncList()
|
|
74
|
-
|
|
67
|
+
this.render()
|
|
75
68
|
})
|
|
76
69
|
|
|
77
70
|
manager.on('name', () => {
|
|
78
71
|
this.syncList()
|
|
79
|
-
|
|
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
|
-
|
|
77
|
+
this.render()
|
|
88
78
|
})
|
|
89
79
|
}
|
|
90
80
|
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
})
|
|
100
|
+
return
|
|
101
|
+
}
|
|
98
102
|
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
})
|
|
105
|
+
return
|
|
106
|
+
}
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
if (str === 'n') {
|
|
107
109
|
this.showAddPrompt()
|
|
108
|
-
|
|
110
|
+
return
|
|
111
|
+
}
|
|
109
112
|
|
|
110
|
-
|
|
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.
|
|
118
|
+
this.render()
|
|
116
119
|
}
|
|
117
|
-
}
|
|
120
|
+
}
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.screen.render()
|
|
127
|
+
if (key.name === 'escape') {
|
|
128
|
+
this.promptState = null
|
|
129
|
+
this.render()
|
|
130
|
+
return
|
|
147
131
|
}
|
|
148
132
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
140
|
+
if (key.name === 'down') {
|
|
141
|
+
state.selectedIndex = (state.selectedIndex + 1) % OverviewUI.AGENT_TYPES.length
|
|
142
|
+
this.render()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
158
145
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
this.
|
|
162
|
+
if (key.name === 'escape') {
|
|
163
|
+
this.promptState = null
|
|
164
|
+
this.render()
|
|
165
|
+
return
|
|
181
166
|
}
|
|
182
167
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
this.
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
:
|
|
372
|
-
const
|
|
373
|
-
if (!
|
|
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 ===
|
|
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.
|
|
413
|
-
this.listBox.focus()
|
|
471
|
+
this.visible = true
|
|
414
472
|
this.syncList()
|
|
415
|
-
this.
|
|
473
|
+
this.render()
|
|
416
474
|
}
|
|
417
475
|
|
|
418
476
|
isPromptOpen(): boolean {
|
|
419
|
-
return this.
|
|
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.
|
|
486
|
+
this.visible = false
|
|
428
487
|
}
|
|
429
488
|
}
|