@k1e1n04/mav 0.1.7 → 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 +272 -227
- 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 +295 -226
- package/src/ui/terminal.ts +104 -0
- package/src/types/neo-blessed.d.ts +0 -5
package/src/ui/overview.ts
CHANGED
|
@@ -1,57 +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,
|
|
51
|
-
mouse: true,
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
this.bindKeys()
|
|
55
50
|
this.syncList()
|
|
56
51
|
|
|
57
52
|
manager.on('data', (sessionId: string) => {
|
|
@@ -59,176 +54,152 @@ export class OverviewUI {
|
|
|
59
54
|
return
|
|
60
55
|
}
|
|
61
56
|
this.syncList()
|
|
62
|
-
|
|
57
|
+
this.render()
|
|
63
58
|
})
|
|
64
59
|
|
|
65
60
|
manager.on('exit', () => {
|
|
66
61
|
this.syncList()
|
|
67
|
-
|
|
62
|
+
this.render()
|
|
68
63
|
})
|
|
69
64
|
|
|
70
65
|
manager.on('status', () => {
|
|
71
66
|
this.syncList()
|
|
72
|
-
|
|
67
|
+
this.render()
|
|
73
68
|
})
|
|
74
69
|
|
|
75
70
|
manager.on('name', () => {
|
|
76
71
|
this.syncList()
|
|
77
|
-
|
|
72
|
+
this.render()
|
|
78
73
|
})
|
|
79
|
-
}
|
|
80
74
|
|
|
81
|
-
|
|
82
|
-
this.listBox.key(['up', 'k'], () => {
|
|
83
|
-
if (this.manager.sessions.length === 0) return
|
|
84
|
-
this.moveSelection(-1)
|
|
75
|
+
manager.on('selection', () => {
|
|
85
76
|
this.syncList()
|
|
86
|
-
this.
|
|
77
|
+
this.render()
|
|
87
78
|
})
|
|
79
|
+
}
|
|
88
80
|
|
|
89
|
-
|
|
90
|
-
|
|
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) {
|
|
99
|
+
this.moveSelection(-1)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ((key.name === 'down' || str === 'j') && this.manager.sessions.length > 0) {
|
|
91
104
|
this.moveSelection(1)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
})
|
|
105
|
+
return
|
|
106
|
+
}
|
|
95
107
|
|
|
96
|
-
|
|
108
|
+
if (str === 'n') {
|
|
97
109
|
this.showAddPrompt()
|
|
98
|
-
|
|
110
|
+
return
|
|
111
|
+
}
|
|
99
112
|
|
|
100
|
-
|
|
113
|
+
if (str === 'd' || (key.ctrl && key.name === 'x')) {
|
|
101
114
|
const session = this.manager.selectedSession
|
|
102
115
|
if (session) {
|
|
103
116
|
this.manager.removeSession(session.id)
|
|
104
117
|
this.syncList()
|
|
105
|
-
this.
|
|
118
|
+
this.render()
|
|
106
119
|
}
|
|
107
|
-
}
|
|
120
|
+
}
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const agentTypes = ['claude-code', 'codex', 'gemini-cli', 'copilot']
|
|
115
|
-
|
|
116
|
-
const prompt = blessed.list({
|
|
117
|
-
parent: this.screen,
|
|
118
|
-
top: 'center',
|
|
119
|
-
left: 'center',
|
|
120
|
-
width: 40,
|
|
121
|
-
height: agentTypes.length + 4,
|
|
122
|
-
border: { type: 'line' },
|
|
123
|
-
label: ' Select agent type ',
|
|
124
|
-
items: agentTypes,
|
|
125
|
-
keys: true,
|
|
126
|
-
style: {
|
|
127
|
-
selected: { bg: 'blue', fg: 'white' },
|
|
128
|
-
border: { fg: 'green' },
|
|
129
|
-
},
|
|
130
|
-
})
|
|
123
|
+
private handleAgentPromptKeypress(key: KeyInfo): void {
|
|
124
|
+
const state = this.promptState
|
|
125
|
+
if (!state || state.mode !== 'agent') return
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.screen.render()
|
|
127
|
+
if (key.name === 'escape') {
|
|
128
|
+
this.promptState = null
|
|
129
|
+
this.render()
|
|
130
|
+
return
|
|
137
131
|
}
|
|
138
132
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})
|
|
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
|
+
}
|
|
146
139
|
|
|
147
|
-
|
|
140
|
+
if (key.name === 'down') {
|
|
141
|
+
state.selectedIndex = (state.selectedIndex + 1) % OverviewUI.AGENT_TYPES.length
|
|
142
|
+
this.render()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
148
145
|
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const box = blessed.box({
|
|
158
|
-
parent: this.screen,
|
|
159
|
-
top: 'center',
|
|
160
|
-
left: 'center',
|
|
161
|
-
width: 60,
|
|
162
|
-
height: 3,
|
|
163
|
-
border: { type: 'line' },
|
|
164
|
-
label: ' cwd Tab:補完 Enter:確定 Esc:キャンセル ',
|
|
165
|
-
style: { border: { fg: 'green' } },
|
|
166
|
-
})
|
|
158
|
+
private handleCwdPromptKeypress(key: KeyInfo): void {
|
|
159
|
+
const state = this.promptState
|
|
160
|
+
if (!state || state.mode !== 'cwd') return
|
|
167
161
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.
|
|
162
|
+
if (key.name === 'escape') {
|
|
163
|
+
this.promptState = null
|
|
164
|
+
this.render()
|
|
165
|
+
return
|
|
171
166
|
}
|
|
172
167
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
175
|
+
}
|
|
176
|
+
|
|
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
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.screen.render()
|
|
185
|
+
state.candidates = []
|
|
186
|
+
|
|
187
|
+
if (key.name === 'backspace') {
|
|
188
|
+
state.value = state.value.slice(0, -1)
|
|
189
|
+
this.render()
|
|
190
|
+
return
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (key.name === 'enter' || key.name === 'return') {
|
|
194
|
-
const expanded = value.trim().replace(/^~/, process.env.HOME ?? '~') || process.cwd()
|
|
195
|
-
cleanup()
|
|
196
|
-
this.spawnAgent(agentType, expanded)
|
|
197
|
-
} else if (key.name === 'escape') {
|
|
198
|
-
cleanup()
|
|
199
|
-
} else if (key.name === 'tab') {
|
|
200
|
-
const { completed, candidates } = completePath(value)
|
|
201
|
-
value = completed
|
|
202
|
-
if (candidates.length > 1) {
|
|
203
|
-
const maxVisible = Math.min(candidates.length, 8)
|
|
204
|
-
candidateList = blessed.list({
|
|
205
|
-
parent: this.screen,
|
|
206
|
-
top: '50%',
|
|
207
|
-
left: 'center',
|
|
208
|
-
width: 60,
|
|
209
|
-
height: maxVisible + 2,
|
|
210
|
-
border: { type: 'line' },
|
|
211
|
-
items: candidates,
|
|
212
|
-
keys: false,
|
|
213
|
-
style: { border: { fg: 'cyan' } },
|
|
214
|
-
}) as Widgets.ListElement
|
|
215
|
-
}
|
|
216
|
-
renderInput()
|
|
217
|
-
} else if (key.name === 'backspace') {
|
|
218
|
-
value = value.slice(0, -1)
|
|
219
|
-
renderInput()
|
|
220
|
-
} else if (!key.ctrl && !key.meta && key.sequence?.length === 1) {
|
|
221
|
-
value += key.sequence
|
|
222
|
-
renderInput()
|
|
223
|
-
}
|
|
193
|
+
if (!key.ctrl && !key.meta && key.sequence?.length === 1) {
|
|
194
|
+
state.value += key.sequence
|
|
195
|
+
this.render()
|
|
224
196
|
}
|
|
197
|
+
}
|
|
225
198
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
box.focus()
|
|
231
|
-
renderInput()
|
|
199
|
+
private showAddPrompt(): void {
|
|
200
|
+
if (this.promptState) return
|
|
201
|
+
this.promptState = { mode: 'agent', selectedIndex: 0 }
|
|
202
|
+
this.render()
|
|
232
203
|
}
|
|
233
204
|
|
|
234
205
|
private spawnAgent(agentType: string, cwd: string): void {
|
|
@@ -258,66 +229,13 @@ export class OverviewUI {
|
|
|
258
229
|
}
|
|
259
230
|
|
|
260
231
|
private showError(message: string): void {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
top: 'center',
|
|
264
|
-
left: 'center',
|
|
265
|
-
width: 50,
|
|
266
|
-
height: message.split('\n').length + 4,
|
|
267
|
-
border: { type: 'line' },
|
|
268
|
-
label: ' Error ',
|
|
269
|
-
content: `\n ${message.split('\n').join('\n ')}`,
|
|
270
|
-
style: { border: { fg: 'red' }, label: { fg: 'red' } },
|
|
271
|
-
keys: true,
|
|
272
|
-
mouse: true,
|
|
273
|
-
})
|
|
274
|
-
overlay.key(['enter', 'escape', 'q'], () => {
|
|
275
|
-
overlay.destroy()
|
|
276
|
-
this.listBox.focus()
|
|
277
|
-
this.screen.render()
|
|
278
|
-
})
|
|
279
|
-
overlay.focus()
|
|
280
|
-
this.screen.render()
|
|
232
|
+
this.promptState = { mode: 'error', message }
|
|
233
|
+
this.render()
|
|
281
234
|
}
|
|
282
235
|
|
|
283
236
|
private syncList(): void {
|
|
284
237
|
const orderedSessions = this.getOrderedSessions()
|
|
285
|
-
|
|
286
|
-
const items: string[] = []
|
|
287
|
-
const selectedId = this.manager.selectedSession?.id
|
|
288
|
-
let selectedDisplayIndex = -1
|
|
289
|
-
|
|
290
|
-
const cwdGroups = this.groupByCwd(orderedSessions)
|
|
291
|
-
for (const [cwd, sessions] of cwdGroups) {
|
|
292
|
-
items.push(` {bold}${this.shortenPath(cwd)}{/bold}`)
|
|
293
|
-
|
|
294
|
-
for (const group of OverviewUI.STATUS_GROUPS) {
|
|
295
|
-
const groupSessions = sessions.filter((s) => s.status === group.status)
|
|
296
|
-
if (groupSessions.length === 0) continue
|
|
297
|
-
|
|
298
|
-
items.push(` ${group.label}`)
|
|
299
|
-
for (const session of groupSessions) {
|
|
300
|
-
const statusIcon =
|
|
301
|
-
session.status === 'running' ? '⣾'
|
|
302
|
-
: session.status === 'idle' ? '○'
|
|
303
|
-
: session.status === 'done' ? '✓'
|
|
304
|
-
: '✗'
|
|
305
|
-
const sessionTitle = session.displayName ?? session.id
|
|
306
|
-
const sessionLabel = session.type ? `${sessionTitle} (${session.type})` : sessionTitle
|
|
307
|
-
const content = `${statusIcon} ${sessionLabel} ${this.getStatusLabel(session.status)}`
|
|
308
|
-
const color = OverviewUI.STATUS_COLORS[session.status]
|
|
309
|
-
items.push(` {${color}-fg}${content}{/${color}-fg}`)
|
|
310
|
-
if (session.id === selectedId) {
|
|
311
|
-
selectedDisplayIndex = items.length - 1
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
this.listBox.setItems(items)
|
|
318
|
-
if (selectedDisplayIndex >= 0) {
|
|
319
|
-
this.listBox.select(selectedDisplayIndex)
|
|
320
|
-
}
|
|
238
|
+
this.displaySessionIds = orderedSessions.map((session) => session.id)
|
|
321
239
|
}
|
|
322
240
|
|
|
323
241
|
private groupByCwd(sessions: SessionManager['sessions']): Map<string, SessionManager['sessions']> {
|
|
@@ -351,18 +269,17 @@ export class OverviewUI {
|
|
|
351
269
|
}
|
|
352
270
|
|
|
353
271
|
private moveSelection(direction: -1 | 1): void {
|
|
354
|
-
const orderedSessions = this.getOrderedSessions()
|
|
355
272
|
const selectedId = this.manager.selectedSession?.id
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
273
|
+
if (this.displaySessionIds.length === 0) return
|
|
274
|
+
|
|
275
|
+
const currentIndex = selectedId ? this.displaySessionIds.indexOf(selectedId) : -1
|
|
359
276
|
const nextIndex = currentIndex === -1
|
|
360
277
|
? 0
|
|
361
|
-
:
|
|
362
|
-
const
|
|
363
|
-
if (!
|
|
278
|
+
: (currentIndex + direction + this.displaySessionIds.length) % this.displaySessionIds.length
|
|
279
|
+
const nextSessionId = this.displaySessionIds[nextIndex]
|
|
280
|
+
if (!nextSessionId) return
|
|
364
281
|
|
|
365
|
-
const managerIndex = this.manager.sessions.findIndex((session) => session.id ===
|
|
282
|
+
const managerIndex = this.manager.sessions.findIndex((session) => session.id === nextSessionId)
|
|
366
283
|
if (managerIndex >= 0) {
|
|
367
284
|
this.manager.selectSession(managerIndex)
|
|
368
285
|
}
|
|
@@ -398,22 +315,174 @@ export class OverviewUI {
|
|
|
398
315
|
}
|
|
399
316
|
}
|
|
400
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
|
+
|
|
401
470
|
show(): void {
|
|
402
|
-
this.
|
|
403
|
-
this.listBox.focus()
|
|
471
|
+
this.visible = true
|
|
404
472
|
this.syncList()
|
|
405
|
-
this.
|
|
473
|
+
this.render()
|
|
406
474
|
}
|
|
407
475
|
|
|
408
476
|
isPromptOpen(): boolean {
|
|
409
|
-
return this.
|
|
477
|
+
return this.promptState != null
|
|
410
478
|
}
|
|
411
479
|
|
|
412
480
|
resizeSelectedSession(): void {
|
|
413
481
|
this.syncList()
|
|
482
|
+
this.render()
|
|
414
483
|
}
|
|
415
484
|
|
|
416
485
|
hide(): void {
|
|
417
|
-
this.
|
|
486
|
+
this.visible = false
|
|
418
487
|
}
|
|
419
488
|
}
|