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