@k1e1n04/mav 0.1.18 → 0.1.20
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 +31 -8
- package/dist/src/ui/overview.js +352 -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 +390 -233
- 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,54 @@
|
|
|
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 HORIZONTAL_MARGIN = 1
|
|
15
|
+
|
|
16
|
+
private static readonly ANSI = {
|
|
17
|
+
reset: '\x1b[0m',
|
|
18
|
+
bold: '\x1b[1m',
|
|
19
|
+
dim: '\x1b[2m',
|
|
20
|
+
fgSlate: '\x1b[38;5;245m',
|
|
21
|
+
fgGreen: '\x1b[38;5;42m',
|
|
22
|
+
fgAmber: '\x1b[38;5;221m',
|
|
23
|
+
fgCyan: '\x1b[38;5;81m',
|
|
24
|
+
fgRed: '\x1b[38;5;203m',
|
|
25
|
+
} as const
|
|
26
|
+
|
|
9
27
|
private static readonly STATUS_GROUPS = [
|
|
10
28
|
{ status: 'running', label: 'Working' },
|
|
11
29
|
{ status: 'idle', label: 'Waiting' },
|
|
12
30
|
{ status: 'done', label: 'Complete' },
|
|
13
31
|
{ status: 'error', label: 'Failed' },
|
|
14
32
|
] as const
|
|
15
|
-
private static readonly STATUS_COLORS = {
|
|
16
|
-
running: 'cyan',
|
|
17
|
-
idle: 'yellow',
|
|
18
|
-
done: 'green',
|
|
19
|
-
error: 'red',
|
|
20
|
-
} as const
|
|
21
33
|
|
|
22
|
-
private
|
|
34
|
+
private static readonly AGENT_TYPES = ['claude-code', 'codex', 'gemini-cli', 'copilot'] as const
|
|
35
|
+
|
|
36
|
+
private terminal: TerminalUI
|
|
23
37
|
private manager: SessionManager
|
|
24
38
|
private onSessionCreated?: (session: SessionManager['selectedSession']) => void
|
|
25
|
-
private
|
|
26
|
-
private
|
|
39
|
+
private promptState: PromptState = null
|
|
40
|
+
private displaySessionIds: string[] = []
|
|
41
|
+
private visible = false
|
|
27
42
|
|
|
28
43
|
constructor(
|
|
29
|
-
|
|
44
|
+
terminal: TerminalUI,
|
|
30
45
|
manager: SessionManager,
|
|
31
46
|
onSessionCreated?: (session: SessionManager['selectedSession']) => void
|
|
32
47
|
) {
|
|
33
|
-
this.
|
|
48
|
+
this.terminal = terminal
|
|
34
49
|
this.manager = manager
|
|
35
50
|
this.onSessionCreated = onSessionCreated
|
|
36
51
|
|
|
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
52
|
this.syncList()
|
|
58
53
|
|
|
59
54
|
manager.on('data', (sessionId: string) => {
|
|
@@ -61,184 +56,152 @@ export class OverviewUI {
|
|
|
61
56
|
return
|
|
62
57
|
}
|
|
63
58
|
this.syncList()
|
|
64
|
-
|
|
59
|
+
this.render()
|
|
65
60
|
})
|
|
66
61
|
|
|
67
62
|
manager.on('exit', () => {
|
|
68
63
|
this.syncList()
|
|
69
|
-
|
|
64
|
+
this.render()
|
|
70
65
|
})
|
|
71
66
|
|
|
72
67
|
manager.on('status', () => {
|
|
73
68
|
this.syncList()
|
|
74
|
-
|
|
69
|
+
this.render()
|
|
75
70
|
})
|
|
76
71
|
|
|
77
72
|
manager.on('name', () => {
|
|
78
73
|
this.syncList()
|
|
79
|
-
|
|
74
|
+
this.render()
|
|
80
75
|
})
|
|
81
76
|
|
|
82
|
-
// removeSession() は emitSelection() を呼ぶが、OverviewUI は 'selection' を
|
|
83
|
-
// listen していないため、index.ts の exit ハンドラ経由で自動削除された場合に
|
|
84
|
-
// UI が再描画されない。'selection' を listen して確実に同期する。
|
|
85
77
|
manager.on('selection', () => {
|
|
86
78
|
this.syncList()
|
|
87
|
-
|
|
79
|
+
this.render()
|
|
88
80
|
})
|
|
89
81
|
}
|
|
90
82
|
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
83
|
+
handleKeypress(str: string, key: KeyInfo): void {
|
|
84
|
+
if (this.promptState?.mode === 'agent') {
|
|
85
|
+
this.handleAgentPromptKeypress(key)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (this.promptState?.mode === 'cwd') {
|
|
89
|
+
this.handleCwdPromptKeypress(key)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if (this.promptState?.mode === 'error') {
|
|
93
|
+
if (key.name === 'enter' || key.name === 'return' || key.name === 'escape' || str === 'q') {
|
|
94
|
+
this.promptState = null
|
|
95
|
+
this.render()
|
|
96
|
+
}
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ((key.name === 'up' || str === 'k') && this.manager.sessions.length > 0) {
|
|
94
101
|
this.moveSelection(-1)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})
|
|
102
|
+
return
|
|
103
|
+
}
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
if (this.manager.sessions.length === 0) return
|
|
105
|
+
if ((key.name === 'down' || str === 'j') && this.manager.sessions.length > 0) {
|
|
101
106
|
this.moveSelection(1)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
})
|
|
107
|
+
return
|
|
108
|
+
}
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
if (str === 'n') {
|
|
107
111
|
this.showAddPrompt()
|
|
108
|
-
|
|
112
|
+
return
|
|
113
|
+
}
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
if (str === 'd' || (key.ctrl && key.name === 'x')) {
|
|
111
116
|
const session = this.manager.selectedSession
|
|
112
117
|
if (session) {
|
|
113
118
|
this.manager.removeSession(session.id)
|
|
114
119
|
this.syncList()
|
|
115
|
-
this.
|
|
120
|
+
this.render()
|
|
116
121
|
}
|
|
117
|
-
}
|
|
122
|
+
}
|
|
118
123
|
}
|
|
119
124
|
|
|
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
|
-
})
|
|
125
|
+
private handleAgentPromptKeypress(key: KeyInfo): void {
|
|
126
|
+
const state = this.promptState
|
|
127
|
+
if (!state || state.mode !== 'agent') return
|
|
141
128
|
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.screen.render()
|
|
129
|
+
if (key.name === 'escape') {
|
|
130
|
+
this.promptState = null
|
|
131
|
+
this.render()
|
|
132
|
+
return
|
|
147
133
|
}
|
|
148
134
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
})
|
|
135
|
+
if (key.name === 'up') {
|
|
136
|
+
state.selectedIndex =
|
|
137
|
+
(state.selectedIndex - 1 + OverviewUI.AGENT_TYPES.length) % OverviewUI.AGENT_TYPES.length
|
|
138
|
+
this.render()
|
|
139
|
+
return
|
|
140
|
+
}
|
|
156
141
|
|
|
157
|
-
|
|
142
|
+
if (key.name === 'down') {
|
|
143
|
+
state.selectedIndex = (state.selectedIndex + 1) % OverviewUI.AGENT_TYPES.length
|
|
144
|
+
this.render()
|
|
145
|
+
return
|
|
146
|
+
}
|
|
158
147
|
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
if (key.name === 'enter' || key.name === 'return') {
|
|
149
|
+
const agentType = OverviewUI.AGENT_TYPES[state.selectedIndex]!
|
|
150
|
+
this.promptState = {
|
|
151
|
+
mode: 'cwd',
|
|
152
|
+
agentType,
|
|
153
|
+
value: process.cwd(),
|
|
154
|
+
candidates: [],
|
|
155
|
+
}
|
|
156
|
+
this.render()
|
|
157
|
+
}
|
|
161
158
|
}
|
|
162
159
|
|
|
163
|
-
private
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
private handleCwdPromptKeypress(key: KeyInfo): void {
|
|
161
|
+
const state = this.promptState
|
|
162
|
+
if (!state || state.mode !== 'cwd') return
|
|
166
163
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
height: 3,
|
|
173
|
-
border: { type: 'line' },
|
|
174
|
-
label: ' cwd Tab:補完 Enter:確定 Esc:キャンセル ',
|
|
175
|
-
style: { border: { fg: 'green' } },
|
|
176
|
-
})
|
|
164
|
+
if (key.name === 'escape') {
|
|
165
|
+
this.promptState = null
|
|
166
|
+
this.render()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
177
169
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
if (key.name === 'enter' || key.name === 'return') {
|
|
171
|
+
const expanded = state.value.trim().replace(/^~/, process.env.HOME ?? '~') || process.cwd()
|
|
172
|
+
const agentType = state.agentType
|
|
173
|
+
this.promptState = null
|
|
174
|
+
this.render()
|
|
175
|
+
this.spawnAgent(agentType, expanded)
|
|
176
|
+
return
|
|
181
177
|
}
|
|
182
178
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
if (key.name === 'tab') {
|
|
180
|
+
const { completed, candidates } = completePath(state.value)
|
|
181
|
+
state.value = completed
|
|
182
|
+
state.candidates = candidates
|
|
183
|
+
this.render()
|
|
184
|
+
return
|
|
188
185
|
}
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.screen.render()
|
|
197
|
-
}
|
|
198
|
-
|
|
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
|
-
}
|
|
187
|
+
state.candidates = []
|
|
188
|
+
|
|
189
|
+
if (key.name === 'backspace') {
|
|
190
|
+
state.value = state.value.slice(0, -1)
|
|
191
|
+
this.render()
|
|
192
|
+
return
|
|
234
193
|
}
|
|
235
194
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
195
|
+
if (!key.ctrl && !key.meta && key.sequence?.length === 1) {
|
|
196
|
+
state.value += key.sequence
|
|
197
|
+
this.render()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private showAddPrompt(): void {
|
|
202
|
+
if (this.promptState) return
|
|
203
|
+
this.promptState = { mode: 'agent', selectedIndex: 0 }
|
|
204
|
+
this.render()
|
|
242
205
|
}
|
|
243
206
|
|
|
244
207
|
private spawnAgent(agentType: string, cwd: string): void {
|
|
@@ -268,66 +231,13 @@ export class OverviewUI {
|
|
|
268
231
|
}
|
|
269
232
|
|
|
270
233
|
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()
|
|
234
|
+
this.promptState = { mode: 'error', message }
|
|
235
|
+
this.render()
|
|
291
236
|
}
|
|
292
237
|
|
|
293
238
|
private syncList(): void {
|
|
294
239
|
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
|
-
}
|
|
240
|
+
this.displaySessionIds = orderedSessions.map((session) => session.id)
|
|
331
241
|
}
|
|
332
242
|
|
|
333
243
|
private groupByCwd(sessions: SessionManager['sessions']): Map<string, SessionManager['sessions']> {
|
|
@@ -361,18 +271,17 @@ export class OverviewUI {
|
|
|
361
271
|
}
|
|
362
272
|
|
|
363
273
|
private moveSelection(direction: -1 | 1): void {
|
|
364
|
-
const orderedSessions = this.getOrderedSessions()
|
|
365
274
|
const selectedId = this.manager.selectedSession?.id
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
275
|
+
if (this.displaySessionIds.length === 0) return
|
|
276
|
+
|
|
277
|
+
const currentIndex = selectedId ? this.displaySessionIds.indexOf(selectedId) : -1
|
|
369
278
|
const nextIndex = currentIndex === -1
|
|
370
279
|
? 0
|
|
371
|
-
:
|
|
372
|
-
const
|
|
373
|
-
if (!
|
|
280
|
+
: (currentIndex + direction + this.displaySessionIds.length) % this.displaySessionIds.length
|
|
281
|
+
const nextSessionId = this.displaySessionIds[nextIndex]
|
|
282
|
+
if (!nextSessionId) return
|
|
374
283
|
|
|
375
|
-
const managerIndex = this.manager.sessions.findIndex((session) => session.id ===
|
|
284
|
+
const managerIndex = this.manager.sessions.findIndex((session) => session.id === nextSessionId)
|
|
376
285
|
if (managerIndex >= 0) {
|
|
377
286
|
this.manager.selectSession(managerIndex)
|
|
378
287
|
}
|
|
@@ -408,22 +317,270 @@ export class OverviewUI {
|
|
|
408
317
|
}
|
|
409
318
|
}
|
|
410
319
|
|
|
320
|
+
private countByStatus(status: string): number {
|
|
321
|
+
return this.manager.sessions.filter((session) => session.status === status).length
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private stripAnsi(value: string): string {
|
|
325
|
+
return value.replace(/\x1b\[[0-9;]*m/g, '')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private visibleLength(value: string): number {
|
|
329
|
+
return this.stripAnsi(value).length
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private getRenderWidth(): number {
|
|
333
|
+
return Math.max(20, this.terminal.cols || 80)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private getContentWidth(): number {
|
|
337
|
+
return Math.max(10, this.getRenderWidth() - (OverviewUI.HORIZONTAL_MARGIN * 2))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private insetLine(line: string): string {
|
|
341
|
+
if (line.length === 0) return ''
|
|
342
|
+
return `${' '.repeat(OverviewUI.HORIZONTAL_MARGIN)}${line}`
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private finalizeLines(lines: string[]): string[] {
|
|
346
|
+
const maxWidth = this.getRenderWidth()
|
|
347
|
+
return lines.map((line) => {
|
|
348
|
+
const inset = this.insetLine(line)
|
|
349
|
+
return this.fitVisible(inset, maxWidth)
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private padRight(value: string, width: number): string {
|
|
354
|
+
const visibleLength = this.visibleLength(value)
|
|
355
|
+
if (visibleLength >= width) {
|
|
356
|
+
return value
|
|
357
|
+
}
|
|
358
|
+
return value + ' '.repeat(width - visibleLength)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private fitPlain(value: string, width: number): string {
|
|
362
|
+
if (width <= 0) return ''
|
|
363
|
+
if (value.length <= width) return value
|
|
364
|
+
if (width === 1) return '…'
|
|
365
|
+
return `${value.slice(0, width - 1)}…`
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private fitVisible(value: string, width: number): string {
|
|
369
|
+
const plain = this.stripAnsi(value)
|
|
370
|
+
return plain.length <= width ? value : this.fitPlain(plain, width)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private color(text: string, ...codes: string[]): string {
|
|
374
|
+
return `${codes.join('')}${text}${OverviewUI.ANSI.reset}`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private getStatusColor(status: string): string {
|
|
378
|
+
switch (status) {
|
|
379
|
+
case 'running':
|
|
380
|
+
return OverviewUI.ANSI.fgGreen
|
|
381
|
+
case 'idle':
|
|
382
|
+
return OverviewUI.ANSI.fgAmber
|
|
383
|
+
case 'done':
|
|
384
|
+
return OverviewUI.ANSI.fgCyan
|
|
385
|
+
case 'error':
|
|
386
|
+
return OverviewUI.ANSI.fgRed
|
|
387
|
+
default:
|
|
388
|
+
return OverviewUI.ANSI.fgSlate
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private makeRule(label: string, width = this.getRenderWidth()): string {
|
|
393
|
+
const text = ` ${label} `
|
|
394
|
+
if (text.length >= width) {
|
|
395
|
+
return this.fitPlain(text, width)
|
|
396
|
+
}
|
|
397
|
+
const fill = '─'.repeat(width - text.length)
|
|
398
|
+
return text + fill
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private wrapPlainText(text: string, width: number): string[] {
|
|
402
|
+
if (width <= 0) return ['']
|
|
403
|
+
const words = text.split(/\s+/).filter(Boolean)
|
|
404
|
+
if (words.length === 0) return ['']
|
|
405
|
+
|
|
406
|
+
const lines: string[] = []
|
|
407
|
+
let current = ''
|
|
408
|
+
for (const word of words) {
|
|
409
|
+
const next = current ? `${current} ${word}` : word
|
|
410
|
+
if (next.length <= width) {
|
|
411
|
+
current = next
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (current) {
|
|
416
|
+
lines.push(current)
|
|
417
|
+
current = ''
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (word.length <= width) {
|
|
421
|
+
current = word
|
|
422
|
+
} else {
|
|
423
|
+
lines.push(this.fitPlain(word, width))
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (current) {
|
|
428
|
+
lines.push(current)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return lines
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private wrapVisibleParts(parts: string[], width: number): string[] {
|
|
435
|
+
if (width <= 0) return ['']
|
|
436
|
+
|
|
437
|
+
const lines: string[] = []
|
|
438
|
+
let current = ''
|
|
439
|
+
for (const part of parts) {
|
|
440
|
+
const next = current ? `${current} · ${part}` : part
|
|
441
|
+
if (this.visibleLength(next) <= width) {
|
|
442
|
+
current = next
|
|
443
|
+
continue
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (current) {
|
|
447
|
+
lines.push(current)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
current = this.visibleLength(part) <= width ? part : this.fitVisible(part, width)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (current) {
|
|
454
|
+
lines.push(current)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return lines
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private formatSummaryLines(width: number): string[] {
|
|
461
|
+
const parts = OverviewUI.STATUS_GROUPS.map((group) => {
|
|
462
|
+
const count = this.countByStatus(group.status)
|
|
463
|
+
return this.color(`${group.label} ${count}`, OverviewUI.ANSI.bold, this.getStatusColor(group.status))
|
|
464
|
+
})
|
|
465
|
+
return this.wrapVisibleParts(parts, width)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private buildMainLines(): string[] {
|
|
469
|
+
const orderedSessions = this.getOrderedSessions()
|
|
470
|
+
const selectedId = this.manager.selectedSession?.id
|
|
471
|
+
const width = this.getContentWidth()
|
|
472
|
+
const lines = [
|
|
473
|
+
this.color(this.makeRule('mav overview', width), OverviewUI.ANSI.dim),
|
|
474
|
+
'AGENTS',
|
|
475
|
+
...this.formatSummaryLines(width),
|
|
476
|
+
'',
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
const cwdGroups = this.groupByCwd(orderedSessions)
|
|
480
|
+
for (const [cwd, sessions] of cwdGroups) {
|
|
481
|
+
lines.push(this.color(this.makeRule(this.shortenPath(cwd)), OverviewUI.ANSI.dim))
|
|
482
|
+
|
|
483
|
+
for (const group of OverviewUI.STATUS_GROUPS) {
|
|
484
|
+
const groupSessions = sessions.filter((s) => s.status === group.status)
|
|
485
|
+
if (groupSessions.length === 0) continue
|
|
486
|
+
|
|
487
|
+
lines.push(this.color(` ${group.label} (${groupSessions.length})`, OverviewUI.ANSI.bold, this.getStatusColor(group.status)))
|
|
488
|
+
for (const session of groupSessions) {
|
|
489
|
+
const color = this.getStatusColor(session.status)
|
|
490
|
+
const statusIcon =
|
|
491
|
+
session.status === 'running' ? '⣾'
|
|
492
|
+
: session.status === 'idle' ? '○'
|
|
493
|
+
: session.status === 'done' ? '✓'
|
|
494
|
+
: '✗'
|
|
495
|
+
const sessionTitle = session.displayName ?? session.id
|
|
496
|
+
const sessionLabel = session.type ? `${sessionTitle} (${session.type})` : sessionTitle
|
|
497
|
+
const prefix = session.id === selectedId ? '> ' : ' '
|
|
498
|
+
const sessionLine = this.fitPlain(
|
|
499
|
+
`${prefix}${statusIcon} ${sessionLabel} ${this.getStatusLabel(session.status)}`,
|
|
500
|
+
width
|
|
501
|
+
)
|
|
502
|
+
lines.push(session.id === selectedId
|
|
503
|
+
? this.color(sessionLine, OverviewUI.ANSI.bold, color)
|
|
504
|
+
: this.color(sessionLine, color))
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
lines.push('')
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (orderedSessions.length === 0) {
|
|
512
|
+
lines.push(this.color(this.makeRule('empty', width), OverviewUI.ANSI.dim))
|
|
513
|
+
lines.push(this.fitPlain(' No sessions. Press n to add one.', width))
|
|
514
|
+
lines.push('')
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
lines.push(this.color(this.makeRule('controls', width), OverviewUI.ANSI.dim))
|
|
518
|
+
lines.push(...this.wrapPlainText('↑/↓ or j/k move Enter detail Ctrl+] back n new d delete q quit', width))
|
|
519
|
+
return this.finalizeLines(lines)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private buildPromptLines(): string[] {
|
|
523
|
+
const state = this.promptState
|
|
524
|
+
if (!state) return []
|
|
525
|
+
|
|
526
|
+
const width = this.getContentWidth()
|
|
527
|
+
const lines = ['', this.color(this.makeRule('prompt', width), OverviewUI.ANSI.dim)]
|
|
528
|
+
if (state.mode === 'agent') {
|
|
529
|
+
lines.push(this.fitPlain('Select agent type', width))
|
|
530
|
+
lines.push('')
|
|
531
|
+
for (const [index, agentType] of OverviewUI.AGENT_TYPES.entries()) {
|
|
532
|
+
lines.push(this.fitPlain(`${index === state.selectedIndex ? '> ' : ' '}${agentType}`, width))
|
|
533
|
+
}
|
|
534
|
+
lines.push('')
|
|
535
|
+
lines.push(...this.wrapPlainText('Enter: select Esc: cancel', width))
|
|
536
|
+
return this.finalizeLines(lines)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (state.mode === 'cwd') {
|
|
540
|
+
lines.push(this.fitPlain(`cwd for ${state.agentType}`, width))
|
|
541
|
+
lines.push(this.fitPlain(state.value, width))
|
|
542
|
+
if (state.candidates.length > 1) {
|
|
543
|
+
lines.push('')
|
|
544
|
+
lines.push(this.fitPlain('Candidates:', width))
|
|
545
|
+
for (const candidate of state.candidates.slice(0, 8)) {
|
|
546
|
+
lines.push(this.fitPlain(` ${candidate}`, width))
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
lines.push('')
|
|
550
|
+
lines.push(...this.wrapPlainText('Tab: complete Enter: confirm Esc: cancel', width))
|
|
551
|
+
return this.finalizeLines(lines)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
lines.push(this.fitPlain('Error', width))
|
|
555
|
+
for (const line of state.message.split('\n')) {
|
|
556
|
+
lines.push(this.fitPlain(line, width))
|
|
557
|
+
}
|
|
558
|
+
lines.push('')
|
|
559
|
+
lines.push(...this.wrapPlainText('Enter/Esc/q: close', width))
|
|
560
|
+
return this.finalizeLines(lines)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private render(): void {
|
|
564
|
+
if (!this.visible) return
|
|
565
|
+
this.terminal.render([...this.buildMainLines(), ...this.buildPromptLines()].join('\n'))
|
|
566
|
+
}
|
|
567
|
+
|
|
411
568
|
show(): void {
|
|
412
|
-
this.
|
|
413
|
-
this.listBox.focus()
|
|
569
|
+
this.visible = true
|
|
414
570
|
this.syncList()
|
|
415
|
-
this.
|
|
571
|
+
this.render()
|
|
416
572
|
}
|
|
417
573
|
|
|
418
574
|
isPromptOpen(): boolean {
|
|
419
|
-
return this.
|
|
575
|
+
return this.promptState != null
|
|
420
576
|
}
|
|
421
577
|
|
|
422
578
|
resizeSelectedSession(): void {
|
|
423
579
|
this.syncList()
|
|
580
|
+
this.render()
|
|
424
581
|
}
|
|
425
582
|
|
|
426
583
|
hide(): void {
|
|
427
|
-
this.
|
|
584
|
+
this.visible = false
|
|
428
585
|
}
|
|
429
586
|
}
|