@felan-ai/felan 0.7.0 → 0.9.0
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/NOTICE +23 -2
- package/README.md +73 -15
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +1 -8
- package/dist/application.js.map +1 -1
- package/dist/extensions.d.ts +9 -3
- package/dist/extensions.d.ts.map +1 -1
- package/dist/extensions.js +34 -2
- package/dist/extensions.js.map +1 -1
- package/dist/mcp/callback-server.d.ts +11 -0
- package/dist/mcp/callback-server.d.ts.map +1 -0
- package/dist/mcp/callback-server.js +221 -0
- package/dist/mcp/callback-server.js.map +1 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +349 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/oauth-host.d.ts +41 -0
- package/dist/mcp/oauth-host.d.ts.map +1 -0
- package/dist/mcp/oauth-host.js +510 -0
- package/dist/mcp/oauth-host.js.map +1 -0
- package/dist/mcp/oauth-store.d.ts +27 -0
- package/dist/mcp/oauth-store.d.ts.map +1 -0
- package/dist/mcp/oauth-store.js +138 -0
- package/dist/mcp/oauth-store.js.map +1 -0
- package/dist/powerline.d.ts +4 -0
- package/dist/powerline.d.ts.map +1 -0
- package/dist/powerline.js +91 -0
- package/dist/powerline.js.map +1 -0
- package/dist/runtime-factory.d.ts +1 -0
- package/dist/runtime-factory.d.ts.map +1 -1
- package/dist/runtime-factory.js +1 -0
- package/dist/runtime-factory.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/subagents/agent-navigator.d.ts +44 -0
- package/dist/subagents/agent-navigator.d.ts.map +1 -0
- package/dist/subagents/agent-navigator.js +667 -0
- package/dist/subagents/agent-navigator.js.map +1 -0
- package/dist/subagents/agent-transcript.d.ts +19 -0
- package/dist/subagents/agent-transcript.d.ts.map +1 -0
- package/dist/subagents/agent-transcript.js +335 -0
- package/dist/subagents/agent-transcript.js.map +1 -0
- package/dist/subagents/catalog.d.ts.map +1 -1
- package/dist/subagents/catalog.js +2 -1
- package/dist/subagents/catalog.js.map +1 -1
- package/dist/subagents/host.d.ts +9 -2
- package/dist/subagents/host.d.ts.map +1 -1
- package/dist/subagents/host.js +40 -8
- package/dist/subagents/host.js.map +1 -1
- package/package.json +12 -6
- package/dist/subagents/presenter.d.ts +0 -11
- package/dist/subagents/presenter.d.ts.map +0 -1
- package/dist/subagents/presenter.js +0 -34
- package/dist/subagents/presenter.js.map +0 -1
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import {} from '@felan-ai/agent-core';
|
|
2
|
+
import { CustomEditor, rawKeyHint, } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import { Input, Key, matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
|
|
4
|
+
import { AgentTranscript } from './agent-transcript.js';
|
|
5
|
+
const MAX_RAIL_ROWS = 5;
|
|
6
|
+
const REFRESH_INTERVAL_MS = 1_000;
|
|
7
|
+
const ACTIVE_RAIL_REFRESH_INTERVAL_MS = 250;
|
|
8
|
+
export const AGENT_NAVIGATOR_SHORTCUT = Key.alt('a');
|
|
9
|
+
export const AGENT_NAVIGATOR_OVERLAY_OPTIONS = {
|
|
10
|
+
anchor: 'top-left',
|
|
11
|
+
width: '100%',
|
|
12
|
+
maxHeight: '100%',
|
|
13
|
+
margin: 0,
|
|
14
|
+
};
|
|
15
|
+
export class AgentRailEditor extends CustomEditor {
|
|
16
|
+
appKeybindings;
|
|
17
|
+
host;
|
|
18
|
+
themeProvider;
|
|
19
|
+
openAgent;
|
|
20
|
+
renderRailInEditor;
|
|
21
|
+
#railFocused = false;
|
|
22
|
+
#selectedAgentId;
|
|
23
|
+
#selectedIndex = 0;
|
|
24
|
+
constructor(tui, editorTheme, appKeybindings, host, themeProvider, openAgent, renderRailInEditor = () => true) {
|
|
25
|
+
super(tui, editorTheme, appKeybindings);
|
|
26
|
+
this.appKeybindings = appKeybindings;
|
|
27
|
+
this.host = host;
|
|
28
|
+
this.themeProvider = themeProvider;
|
|
29
|
+
this.openAgent = openAgent;
|
|
30
|
+
this.renderRailInEditor = renderRailInEditor;
|
|
31
|
+
}
|
|
32
|
+
handleInput(data) {
|
|
33
|
+
const records = activeSubagents(this.host);
|
|
34
|
+
this.#syncSelection(records);
|
|
35
|
+
if (this.#railFocused) {
|
|
36
|
+
if (this.appKeybindings.matches(data, 'tui.editor.cursorUp')) {
|
|
37
|
+
if (this.#selectedIndex === 0)
|
|
38
|
+
this.#railFocused = false;
|
|
39
|
+
else
|
|
40
|
+
this.#select(records, this.#selectedIndex - 1);
|
|
41
|
+
this.tui.requestRender();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (this.appKeybindings.matches(data, 'tui.editor.cursorDown')) {
|
|
45
|
+
this.#select(records, Math.min(records.length - 1, this.#selectedIndex + 1));
|
|
46
|
+
this.tui.requestRender();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (matchesKey(data, Key.enter)) {
|
|
50
|
+
const selected = records[this.#selectedIndex];
|
|
51
|
+
if (selected)
|
|
52
|
+
this.openAgent(selected.agentId);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (matchesKey(data, Key.escape)) {
|
|
56
|
+
this.#railFocused = false;
|
|
57
|
+
this.tui.requestRender();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.#railFocused = false;
|
|
61
|
+
}
|
|
62
|
+
if (records.length > 0
|
|
63
|
+
&& !this.isShowingAutocomplete()
|
|
64
|
+
&& this.appKeybindings.matches(data, 'tui.editor.cursorDown')) {
|
|
65
|
+
const beforeText = this.getText();
|
|
66
|
+
const beforeCursor = this.getCursor();
|
|
67
|
+
super.handleInput(data);
|
|
68
|
+
const afterCursor = this.getCursor();
|
|
69
|
+
if (this.getText() === beforeText
|
|
70
|
+
&& afterCursor.line === beforeCursor.line
|
|
71
|
+
&& afterCursor.col === beforeCursor.col) {
|
|
72
|
+
this.#railFocused = true;
|
|
73
|
+
this.#select(records, 0);
|
|
74
|
+
this.tui.requestRender();
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
super.handleInput(data);
|
|
79
|
+
}
|
|
80
|
+
render(width) {
|
|
81
|
+
const records = activeSubagents(this.host);
|
|
82
|
+
this.#syncSelection(records);
|
|
83
|
+
const wasFocused = this.focused;
|
|
84
|
+
if (this.#railFocused)
|
|
85
|
+
this.focused = false;
|
|
86
|
+
let editorLines;
|
|
87
|
+
try {
|
|
88
|
+
editorLines = super.render(width);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.focused = wasFocused;
|
|
92
|
+
}
|
|
93
|
+
if (this.#railFocused)
|
|
94
|
+
hideEditorCursor(editorLines);
|
|
95
|
+
return this.renderRailInEditor()
|
|
96
|
+
? [...editorLines, ...this.renderRail(width)]
|
|
97
|
+
: editorLines;
|
|
98
|
+
}
|
|
99
|
+
renderRail(width) {
|
|
100
|
+
if (width <= 0)
|
|
101
|
+
return [];
|
|
102
|
+
const records = activeSubagents(this.host);
|
|
103
|
+
this.#syncSelection(records);
|
|
104
|
+
const theme = this.themeProvider();
|
|
105
|
+
return records.map((record, index) => renderAgentRow(record, this.#railFocused && index === this.#selectedIndex, theme, width));
|
|
106
|
+
}
|
|
107
|
+
refresh() {
|
|
108
|
+
this.#syncSelection(activeSubagents(this.host));
|
|
109
|
+
this.tui.requestRender();
|
|
110
|
+
}
|
|
111
|
+
#syncSelection(records) {
|
|
112
|
+
if (records.length === 0) {
|
|
113
|
+
this.#railFocused = false;
|
|
114
|
+
this.#selectedAgentId = undefined;
|
|
115
|
+
this.#selectedIndex = 0;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const selectedIndex = records.findIndex((record) => record.agentId === this.#selectedAgentId);
|
|
119
|
+
this.#select(records, selectedIndex < 0 ? Math.min(this.#selectedIndex, records.length - 1) : selectedIndex);
|
|
120
|
+
}
|
|
121
|
+
#select(records, index) {
|
|
122
|
+
this.#selectedIndex = Math.max(0, Math.min(records.length - 1, index));
|
|
123
|
+
this.#selectedAgentId = records[this.#selectedIndex]?.agentId;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export class AgentNavigator {
|
|
127
|
+
tui;
|
|
128
|
+
host;
|
|
129
|
+
theme;
|
|
130
|
+
keybindings;
|
|
131
|
+
done;
|
|
132
|
+
#input = new Input();
|
|
133
|
+
#records = [];
|
|
134
|
+
#selectedId;
|
|
135
|
+
#transcript;
|
|
136
|
+
#transcriptSession;
|
|
137
|
+
#refreshInterval;
|
|
138
|
+
#inputFocused = false;
|
|
139
|
+
#closed = false;
|
|
140
|
+
#statusMessage = '';
|
|
141
|
+
#scrollOffset = 0;
|
|
142
|
+
#autoScroll = true;
|
|
143
|
+
#lastContentLength = 0;
|
|
144
|
+
#lastViewportHeight = 1;
|
|
145
|
+
#focused = false;
|
|
146
|
+
constructor(tui, host, theme, keybindings, done, initialAgentId) {
|
|
147
|
+
this.tui = tui;
|
|
148
|
+
this.host = host;
|
|
149
|
+
this.theme = theme;
|
|
150
|
+
this.keybindings = keybindings;
|
|
151
|
+
this.done = done;
|
|
152
|
+
this.#records = [...host.listLocalSubagents()];
|
|
153
|
+
this.#selectedId = initialAgentId && this.#records.some((record) => record.agentId === initialAgentId)
|
|
154
|
+
? initialAgentId
|
|
155
|
+
: this.#initialSelection(this.#records);
|
|
156
|
+
this.#input.onSubmit = (value) => {
|
|
157
|
+
void this.#submitSteer(value);
|
|
158
|
+
};
|
|
159
|
+
this.#input.onEscape = () => this.close();
|
|
160
|
+
this.#syncSelectedRecord();
|
|
161
|
+
this.#refreshInterval = setInterval(() => {
|
|
162
|
+
if (this.#closed)
|
|
163
|
+
return;
|
|
164
|
+
this.#refreshRecords();
|
|
165
|
+
this.tui.requestRender();
|
|
166
|
+
}, REFRESH_INTERVAL_MS);
|
|
167
|
+
this.#refreshInterval.unref?.();
|
|
168
|
+
}
|
|
169
|
+
get focused() {
|
|
170
|
+
return this.#focused;
|
|
171
|
+
}
|
|
172
|
+
set focused(value) {
|
|
173
|
+
this.#focused = value;
|
|
174
|
+
this.#input.focused = value && this.#inputFocused;
|
|
175
|
+
}
|
|
176
|
+
close() {
|
|
177
|
+
if (this.#closed)
|
|
178
|
+
return;
|
|
179
|
+
this.#closed = true;
|
|
180
|
+
this.done();
|
|
181
|
+
}
|
|
182
|
+
handleInput(data) {
|
|
183
|
+
if (matchesKey(data, Key.escape)) {
|
|
184
|
+
this.close();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (this.#inputFocused) {
|
|
188
|
+
if (matchesKey(data, Key.tab))
|
|
189
|
+
this.#focusNavigation();
|
|
190
|
+
else
|
|
191
|
+
this.#input.handleInput(data);
|
|
192
|
+
this.tui.requestRender();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.#transcript?.handleInput(data))
|
|
196
|
+
return;
|
|
197
|
+
if (matchesKey(data, Key.up) || matchesKey(data, 'k')) {
|
|
198
|
+
this.#moveSelection(-1);
|
|
199
|
+
}
|
|
200
|
+
else if (matchesKey(data, Key.down) || matchesKey(data, 'j')) {
|
|
201
|
+
this.#moveSelection(1);
|
|
202
|
+
}
|
|
203
|
+
else if (matchesKey(data, Key.pageUp)) {
|
|
204
|
+
this.#scrollBy(-this.#lastViewportHeight);
|
|
205
|
+
}
|
|
206
|
+
else if (matchesKey(data, Key.pageDown)) {
|
|
207
|
+
this.#scrollBy(this.#lastViewportHeight);
|
|
208
|
+
}
|
|
209
|
+
else if (matchesKey(data, Key.home)) {
|
|
210
|
+
this.#autoScroll = false;
|
|
211
|
+
this.#scrollOffset = 0;
|
|
212
|
+
}
|
|
213
|
+
else if (matchesKey(data, Key.end)) {
|
|
214
|
+
this.#autoScroll = true;
|
|
215
|
+
}
|
|
216
|
+
else if (matchesKey(data, Key.enter) || matchesKey(data, Key.tab)) {
|
|
217
|
+
this.#focusInput();
|
|
218
|
+
}
|
|
219
|
+
else if (matchesKey(data, 'q')) {
|
|
220
|
+
this.close();
|
|
221
|
+
}
|
|
222
|
+
else if (matchesKey(data, 'x') || matchesKey(data, 's')) {
|
|
223
|
+
void this.#stopSelected();
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.tui.requestRender();
|
|
229
|
+
}
|
|
230
|
+
render(width) {
|
|
231
|
+
if (width <= 0)
|
|
232
|
+
return [];
|
|
233
|
+
this.#refreshRecords();
|
|
234
|
+
const rows = Math.max(1, this.tui.terminal.rows);
|
|
235
|
+
if (this.#inputFocused && rows < 5)
|
|
236
|
+
this.#focusNavigation(true);
|
|
237
|
+
const selected = this.#selectedRecord();
|
|
238
|
+
const canSteer = selected?.status === 'running';
|
|
239
|
+
const showStatus = Boolean(this.#statusMessage) && rows >= 7;
|
|
240
|
+
const showPrompt = canSteer && rows >= 5;
|
|
241
|
+
const showDivider = rows >= 6;
|
|
242
|
+
const showHints = rows >= 4;
|
|
243
|
+
const fixedRows = 1
|
|
244
|
+
+ Number(showStatus)
|
|
245
|
+
+ Number(showPrompt)
|
|
246
|
+
+ Number(showDivider)
|
|
247
|
+
+ Number(showHints);
|
|
248
|
+
const contentBudget = Math.max(0, rows - fixedRows);
|
|
249
|
+
let railRows = contentBudget > 1
|
|
250
|
+
? Math.min(MAX_RAIL_ROWS, this.#records.length, Math.max(1, contentBudget - 1))
|
|
251
|
+
: 0;
|
|
252
|
+
let viewportHeight = contentBudget - railRows;
|
|
253
|
+
if (viewportHeight < 1 && railRows > 0 && contentBudget > 1) {
|
|
254
|
+
railRows -= 1;
|
|
255
|
+
viewportHeight = 1;
|
|
256
|
+
}
|
|
257
|
+
this.#lastViewportHeight = Math.max(1, viewportHeight);
|
|
258
|
+
const lines = [this.#renderHeader(selected)];
|
|
259
|
+
if (showStatus)
|
|
260
|
+
lines.push(this.theme.fg('warning', this.#statusMessage));
|
|
261
|
+
lines.push(...this.#renderTranscript(width, viewportHeight, selected));
|
|
262
|
+
if (showDivider)
|
|
263
|
+
lines.push(this.theme.fg('border', '─'.repeat(width)));
|
|
264
|
+
if (showPrompt && selected)
|
|
265
|
+
lines.push(this.#renderInput(width, selected));
|
|
266
|
+
lines.push(...this.#renderRail(width, railRows));
|
|
267
|
+
if (showHints)
|
|
268
|
+
lines.push(this.#renderHints());
|
|
269
|
+
while (lines.length < rows) {
|
|
270
|
+
lines.splice(Math.max(1, lines.length - railRows - Number(showHints)), 0, '');
|
|
271
|
+
}
|
|
272
|
+
return lines.slice(0, rows).map((line) => this.#fitLine(line, width));
|
|
273
|
+
}
|
|
274
|
+
invalidate() {
|
|
275
|
+
this.#transcript?.invalidate();
|
|
276
|
+
}
|
|
277
|
+
dispose() {
|
|
278
|
+
this.#closed = true;
|
|
279
|
+
if (this.#refreshInterval) {
|
|
280
|
+
clearInterval(this.#refreshInterval);
|
|
281
|
+
this.#refreshInterval = undefined;
|
|
282
|
+
}
|
|
283
|
+
this.#focusNavigation(true);
|
|
284
|
+
this.#transcript?.dispose();
|
|
285
|
+
this.#transcript = undefined;
|
|
286
|
+
this.#transcriptSession = undefined;
|
|
287
|
+
}
|
|
288
|
+
#initialSelection(records) {
|
|
289
|
+
return records.find((record) => record.status === 'running' || record.status === 'queued')?.agentId
|
|
290
|
+
?? records[0]?.agentId;
|
|
291
|
+
}
|
|
292
|
+
#refreshRecords() {
|
|
293
|
+
const previousIndex = Math.max(0, this.#records.findIndex((record) => record.agentId === this.#selectedId));
|
|
294
|
+
this.#records = [...this.host.listLocalSubagents()];
|
|
295
|
+
if (this.#selectedId && !this.#records.some((record) => record.agentId === this.#selectedId)) {
|
|
296
|
+
if (this.#inputFocused)
|
|
297
|
+
this.#focusNavigation(true);
|
|
298
|
+
this.#selectedId = this.#records[Math.min(previousIndex, this.#records.length - 1)]?.agentId;
|
|
299
|
+
this.#statusMessage = '';
|
|
300
|
+
}
|
|
301
|
+
else if (!this.#selectedId && this.#records.length > 0) {
|
|
302
|
+
this.#selectedId = this.#initialSelection(this.#records);
|
|
303
|
+
}
|
|
304
|
+
const selected = this.#selectedRecord();
|
|
305
|
+
if (this.#inputFocused && selected?.status !== 'running')
|
|
306
|
+
this.#focusNavigation(true);
|
|
307
|
+
this.#syncSelectedRecord();
|
|
308
|
+
}
|
|
309
|
+
#syncSelectedRecord() {
|
|
310
|
+
const session = this.#selectedRecord()?.session;
|
|
311
|
+
if (session === this.#transcriptSession)
|
|
312
|
+
return;
|
|
313
|
+
this.#transcript?.dispose();
|
|
314
|
+
this.#transcript = undefined;
|
|
315
|
+
this.#transcriptSession = session;
|
|
316
|
+
this.#scrollOffset = 0;
|
|
317
|
+
this.#autoScroll = true;
|
|
318
|
+
if (!session)
|
|
319
|
+
return;
|
|
320
|
+
try {
|
|
321
|
+
this.#transcript = new AgentTranscript(this.tui, this.keybindings);
|
|
322
|
+
this.#transcript.attach(session);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
this.#transcript?.dispose();
|
|
326
|
+
this.#transcript = undefined;
|
|
327
|
+
this.#statusMessage = `Unable to render transcript: ${error instanceof Error ? error.message : String(error)}`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
#selectedRecord() {
|
|
331
|
+
return this.#selectedId ? this.host.getLocalSubagent(this.#selectedId) : undefined;
|
|
332
|
+
}
|
|
333
|
+
#moveSelection(delta) {
|
|
334
|
+
if (this.#records.length === 0)
|
|
335
|
+
return;
|
|
336
|
+
const current = Math.max(0, this.#records.findIndex((record) => record.agentId === this.#selectedId));
|
|
337
|
+
const next = Math.max(0, Math.min(this.#records.length - 1, current + delta));
|
|
338
|
+
if (next === current)
|
|
339
|
+
return;
|
|
340
|
+
this.#focusNavigation(true);
|
|
341
|
+
this.#selectedId = this.#records[next].agentId;
|
|
342
|
+
this.#statusMessage = '';
|
|
343
|
+
this.#syncSelectedRecord();
|
|
344
|
+
}
|
|
345
|
+
#focusInput() {
|
|
346
|
+
if (this.#selectedRecord()?.status !== 'running' || this.tui.terminal.rows < 5)
|
|
347
|
+
return;
|
|
348
|
+
this.#inputFocused = true;
|
|
349
|
+
this.#input.focused = this.focused;
|
|
350
|
+
}
|
|
351
|
+
#focusNavigation(clearDraft = false) {
|
|
352
|
+
this.#inputFocused = false;
|
|
353
|
+
this.#input.focused = false;
|
|
354
|
+
if (clearDraft)
|
|
355
|
+
this.#input.setValue('');
|
|
356
|
+
}
|
|
357
|
+
async #stopSelected() {
|
|
358
|
+
const record = this.#selectedRecord();
|
|
359
|
+
if (!record || (record.status !== 'running' && record.status !== 'queued')) {
|
|
360
|
+
this.#statusMessage = record
|
|
361
|
+
? `Subagent is ${formatStatus(record.status)}.`
|
|
362
|
+
: 'Select a running or queued subagent first.';
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.#statusMessage = `Stopping ${record.type}…`;
|
|
366
|
+
this.tui.requestRender();
|
|
367
|
+
const result = await this.host.cancel(record.agentId, 'Stopped from the agent navigator');
|
|
368
|
+
if (this.#closed)
|
|
369
|
+
return;
|
|
370
|
+
this.#statusMessage = result.ok
|
|
371
|
+
? `Stopped ${record.type}.`
|
|
372
|
+
: result.error.message;
|
|
373
|
+
this.#refreshRecords();
|
|
374
|
+
this.tui.requestRender();
|
|
375
|
+
}
|
|
376
|
+
async #submitSteer(value) {
|
|
377
|
+
const message = value.trim();
|
|
378
|
+
const record = this.#selectedRecord();
|
|
379
|
+
if (!message || !record || record.status !== 'running')
|
|
380
|
+
return;
|
|
381
|
+
const agentId = record.agentId;
|
|
382
|
+
const result = await this.host.steer(agentId, message);
|
|
383
|
+
if (this.#closed)
|
|
384
|
+
return;
|
|
385
|
+
this.#statusMessage = result.ok
|
|
386
|
+
? `Message sent to ${record.type}.`
|
|
387
|
+
: result.error.message;
|
|
388
|
+
if (result.ok && this.#selectedId === agentId && this.#input.getValue() === value) {
|
|
389
|
+
this.#input.setValue('');
|
|
390
|
+
}
|
|
391
|
+
this.tui.requestRender();
|
|
392
|
+
}
|
|
393
|
+
#scrollBy(delta) {
|
|
394
|
+
const maxScroll = Math.max(0, this.#lastContentLength - this.#lastViewportHeight);
|
|
395
|
+
this.#autoScroll = false;
|
|
396
|
+
this.#scrollOffset = Math.max(0, Math.min(maxScroll, this.#scrollOffset + delta));
|
|
397
|
+
if (this.#scrollOffset >= maxScroll)
|
|
398
|
+
this.#autoScroll = true;
|
|
399
|
+
}
|
|
400
|
+
#renderTranscript(width, height, selected) {
|
|
401
|
+
if (height <= 0)
|
|
402
|
+
return [];
|
|
403
|
+
let content;
|
|
404
|
+
if (!selected) {
|
|
405
|
+
content = [this.theme.fg('dim', 'No subagents to show. Press Esc/q to close.')];
|
|
406
|
+
}
|
|
407
|
+
else if (!selected.session) {
|
|
408
|
+
content = this.#renderUnavailableTranscript(selected, width);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
content = this.#transcript?.render(width) ?? [];
|
|
412
|
+
if (content.length === 0)
|
|
413
|
+
content = [this.theme.fg('dim', 'Waiting for the first message…')];
|
|
414
|
+
}
|
|
415
|
+
this.#lastContentLength = content.length;
|
|
416
|
+
const maxScroll = Math.max(0, content.length - height);
|
|
417
|
+
if (this.#autoScroll)
|
|
418
|
+
this.#scrollOffset = maxScroll;
|
|
419
|
+
else
|
|
420
|
+
this.#scrollOffset = Math.min(this.#scrollOffset, maxScroll);
|
|
421
|
+
const visible = content.slice(this.#scrollOffset, this.#scrollOffset + height);
|
|
422
|
+
return [...visible, ...Array(Math.max(0, height - visible.length)).fill('')];
|
|
423
|
+
}
|
|
424
|
+
#renderUnavailableTranscript(record, width) {
|
|
425
|
+
if (record.result) {
|
|
426
|
+
const text = stripTerminalSequences(record.result);
|
|
427
|
+
return text.split('\n').flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width)));
|
|
428
|
+
}
|
|
429
|
+
if (record.error)
|
|
430
|
+
return [this.theme.fg('error', record.error.message)];
|
|
431
|
+
const state = record.status === 'queued'
|
|
432
|
+
? 'Waiting for an execution slot…'
|
|
433
|
+
: `Session unavailable (${formatStatus(record.status)}).`;
|
|
434
|
+
return [this.theme.fg('dim', state)];
|
|
435
|
+
}
|
|
436
|
+
#renderHeader(selected) {
|
|
437
|
+
if (!selected) {
|
|
438
|
+
return this.theme.bold('Agent navigator') + this.theme.fg('dim', ' · no subagents · Esc closes');
|
|
439
|
+
}
|
|
440
|
+
return this.theme.bold(`Viewing ${selected.type}`)
|
|
441
|
+
+ this.theme.fg('dim', ` · ${describeActivity(selected)} · ${formatDuration(selected)} · Esc returns`);
|
|
442
|
+
}
|
|
443
|
+
#renderInput(width, selected) {
|
|
444
|
+
const fullLabel = `Message ${selected.type}… `;
|
|
445
|
+
const labelText = visibleWidth(fullLabel) <= Math.max(1, width - 4) ? fullLabel : '> ';
|
|
446
|
+
const label = this.theme.fg('accent', labelText);
|
|
447
|
+
const inputWidth = Math.max(1, width - visibleWidth(label));
|
|
448
|
+
return label + (this.#input.render(inputWidth)[0] ?? '');
|
|
449
|
+
}
|
|
450
|
+
#renderRail(width, maxRows) {
|
|
451
|
+
if (maxRows <= 0)
|
|
452
|
+
return [];
|
|
453
|
+
const selectedIndex = Math.max(0, this.#records.findIndex((record) => record.agentId === this.#selectedId));
|
|
454
|
+
const windowSize = Math.min(maxRows, this.#records.length);
|
|
455
|
+
const maxStart = Math.max(0, this.#records.length - windowSize);
|
|
456
|
+
const start = Math.max(0, Math.min(maxStart, selectedIndex - Math.floor(windowSize / 2)));
|
|
457
|
+
const visibleItems = this.#records.slice(start, start + windowSize);
|
|
458
|
+
return visibleItems.map((item, index) => {
|
|
459
|
+
const indicators = [];
|
|
460
|
+
if (index === 0 && start > 0)
|
|
461
|
+
indicators.push(`↑${start}`);
|
|
462
|
+
const below = this.#records.length - (start + windowSize);
|
|
463
|
+
if (index === visibleItems.length - 1 && below > 0)
|
|
464
|
+
indicators.push(`↓${below}`);
|
|
465
|
+
const indicator = indicators.length > 0
|
|
466
|
+
? this.theme.fg('dim', `${indicators.join(' ')} `)
|
|
467
|
+
: '';
|
|
468
|
+
return indicator + this.#renderRailItem(item, item.agentId === this.#selectedId, Math.max(0, width - visibleWidth(indicator)));
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
#renderRailItem(item, selected, width) {
|
|
472
|
+
return renderAgentRow(item, selected, this.theme, width);
|
|
473
|
+
}
|
|
474
|
+
#renderHints() {
|
|
475
|
+
const hints = [
|
|
476
|
+
rawKeyHint('↑↓', 'select'),
|
|
477
|
+
rawKeyHint('Enter/Tab', 'view/message'),
|
|
478
|
+
rawKeyHint('PgUp/PgDn', 'scroll'),
|
|
479
|
+
rawKeyHint('x', 'stop'),
|
|
480
|
+
rawKeyHint('q/Esc', 'close'),
|
|
481
|
+
];
|
|
482
|
+
if (this.#transcript) {
|
|
483
|
+
const transcriptHints = this.#transcript.getToggleHints();
|
|
484
|
+
hints.splice(3, 0, transcriptHints.tools, transcriptHints.thinking);
|
|
485
|
+
}
|
|
486
|
+
return hints.join(this.theme.fg('dim', ' · '));
|
|
487
|
+
}
|
|
488
|
+
#fitLine(line, width) {
|
|
489
|
+
const truncated = truncateToWidth(line, width);
|
|
490
|
+
return truncated + ' '.repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
export async function openAgentNavigator(ctx, host, initialAgentId) {
|
|
494
|
+
let navigator;
|
|
495
|
+
let escapeRequested = false;
|
|
496
|
+
const unsubscribe = ctx.ui.onTerminalInput((data) => {
|
|
497
|
+
if (!matchesKey(data, Key.escape))
|
|
498
|
+
return undefined;
|
|
499
|
+
escapeRequested = true;
|
|
500
|
+
navigator?.close();
|
|
501
|
+
return { consume: true };
|
|
502
|
+
});
|
|
503
|
+
try {
|
|
504
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
505
|
+
navigator = new AgentNavigator(tui, host, theme, keybindings, () => done(undefined), initialAgentId);
|
|
506
|
+
if (escapeRequested)
|
|
507
|
+
queueMicrotask(() => navigator?.close());
|
|
508
|
+
return navigator;
|
|
509
|
+
}, { overlay: true, overlayOptions: AGENT_NAVIGATOR_OVERLAY_OPTIONS });
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
unsubscribe();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
export function registerLocalSubagentNavigator(pi, host, options = {}) {
|
|
516
|
+
let controlsRegistered = false;
|
|
517
|
+
let stopRail = () => { };
|
|
518
|
+
pi.on('session_start', (_event, ctx) => {
|
|
519
|
+
stopRail();
|
|
520
|
+
if (ctx.mode !== 'tui')
|
|
521
|
+
return;
|
|
522
|
+
const open = (commandContext) => openAgentNavigator(commandContext, host);
|
|
523
|
+
if (!controlsRegistered) {
|
|
524
|
+
controlsRegistered = true;
|
|
525
|
+
pi.registerCommand('agents', {
|
|
526
|
+
description: 'Open the subagent navigator',
|
|
527
|
+
handler: async (_args, commandContext) => open(commandContext),
|
|
528
|
+
});
|
|
529
|
+
pi.registerShortcut(AGENT_NAVIGATOR_SHORTCUT, {
|
|
530
|
+
description: 'Open the subagent navigator',
|
|
531
|
+
handler: open,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
let editor;
|
|
535
|
+
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
|
|
536
|
+
editor = new AgentRailEditor(tui, editorTheme, keybindings, host, () => ctx.ui.theme, (agentId) => {
|
|
537
|
+
void openAgentNavigator(ctx, host, agentId);
|
|
538
|
+
}, options.renderRailInEditor ?? (() => true));
|
|
539
|
+
return editor;
|
|
540
|
+
});
|
|
541
|
+
options.onRailRendererChange?.((width) => editor?.renderRail(width) ?? []);
|
|
542
|
+
let hadActiveSubagents = activeSubagents(host).length > 0;
|
|
543
|
+
const refreshInterval = setInterval(() => {
|
|
544
|
+
const hasActiveSubagents = activeSubagents(host).length > 0;
|
|
545
|
+
if (hasActiveSubagents || hadActiveSubagents)
|
|
546
|
+
editor?.refresh();
|
|
547
|
+
hadActiveSubagents = hasActiveSubagents;
|
|
548
|
+
}, ACTIVE_RAIL_REFRESH_INTERVAL_MS);
|
|
549
|
+
refreshInterval.unref?.();
|
|
550
|
+
stopRail = () => {
|
|
551
|
+
clearInterval(refreshInterval);
|
|
552
|
+
editor = undefined;
|
|
553
|
+
options.onRailRendererChange?.(undefined);
|
|
554
|
+
stopRail = () => { };
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
pi.on('session_shutdown', () => stopRail());
|
|
558
|
+
}
|
|
559
|
+
function activeSubagents(host) {
|
|
560
|
+
return [...host.listLocalSubagents()]
|
|
561
|
+
.filter((record) => record.status === 'queued' || record.status === 'running');
|
|
562
|
+
}
|
|
563
|
+
function renderAgentRow(record, selected, theme, width) {
|
|
564
|
+
if (width <= 0)
|
|
565
|
+
return '';
|
|
566
|
+
const cursor = selected ? theme.fg('accent', '›') : ' ';
|
|
567
|
+
const name = selected ? theme.bold(record.type) : record.type;
|
|
568
|
+
const head = `${cursor} ${statusIcon(record, theme)} ${name}`;
|
|
569
|
+
const duration = formatDuration(record);
|
|
570
|
+
const durationWidth = visibleWidth(duration);
|
|
571
|
+
const summary = record.status === 'running'
|
|
572
|
+
? describeActivity(record)
|
|
573
|
+
: activityLine(record.description);
|
|
574
|
+
const summaryWidth = Math.max(0, width - visibleWidth(head) - 2 - (durationWidth > 0 ? durationWidth + 2 : 0));
|
|
575
|
+
const renderedSummary = summaryWidth > 0
|
|
576
|
+
? theme.fg('dim', truncateToWidth(summary, summaryWidth, '…'))
|
|
577
|
+
: '';
|
|
578
|
+
const left = truncateToWidth(renderedSummary ? `${head} ${renderedSummary}` : head, width, '');
|
|
579
|
+
const available = width - visibleWidth(left);
|
|
580
|
+
const renderedDuration = durationWidth > 0 && available >= durationWidth + 1
|
|
581
|
+
? `${' '.repeat(Math.max(2, available - durationWidth))}${theme.fg('dim', duration)}`
|
|
582
|
+
: '';
|
|
583
|
+
const line = truncateToWidth(left + renderedDuration, width, '');
|
|
584
|
+
return line + ' '.repeat(Math.max(0, width - visibleWidth(line)));
|
|
585
|
+
}
|
|
586
|
+
function hideEditorCursor(lines) {
|
|
587
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
588
|
+
const line = lines[index];
|
|
589
|
+
const start = line.indexOf('\x1b[7m');
|
|
590
|
+
if (start < 0)
|
|
591
|
+
continue;
|
|
592
|
+
const end = line.indexOf('\x1b[0m', start + 4);
|
|
593
|
+
if (end < 0)
|
|
594
|
+
return;
|
|
595
|
+
lines[index] = line.slice(0, start) + line.slice(start + 4, end) + line.slice(end + 4);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function statusIcon(record, theme) {
|
|
600
|
+
if (record.status === 'running')
|
|
601
|
+
return theme.fg('accent', '●');
|
|
602
|
+
if (record.status === 'queued')
|
|
603
|
+
return theme.fg('muted', '◦');
|
|
604
|
+
if (record.status === 'completed')
|
|
605
|
+
return theme.fg('success', '✓');
|
|
606
|
+
if (record.status === 'failed')
|
|
607
|
+
return theme.fg('error', '✗');
|
|
608
|
+
if (record.status === 'timed_out')
|
|
609
|
+
return theme.fg('warning', '◷');
|
|
610
|
+
return theme.fg('dim', '■');
|
|
611
|
+
}
|
|
612
|
+
function describeActivity(record) {
|
|
613
|
+
if (record.status !== 'running')
|
|
614
|
+
return formatStatus(record.status);
|
|
615
|
+
const session = record.session;
|
|
616
|
+
if (!session)
|
|
617
|
+
return 'starting…';
|
|
618
|
+
const pendingToolCalls = session.state.pendingToolCalls;
|
|
619
|
+
if (pendingToolCalls.size > 0) {
|
|
620
|
+
const toolNames = new Set();
|
|
621
|
+
const messages = session.state.streamingMessage
|
|
622
|
+
? [...session.messages, session.state.streamingMessage]
|
|
623
|
+
: session.messages;
|
|
624
|
+
for (const message of messages) {
|
|
625
|
+
if (message.role !== 'assistant')
|
|
626
|
+
continue;
|
|
627
|
+
for (const content of message.content) {
|
|
628
|
+
if (content.type === 'toolCall' && pendingToolCalls.has(content.id))
|
|
629
|
+
toolNames.add(content.name);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return toolNames.size > 0
|
|
633
|
+
? `using ${[...toolNames].join(', ')}…`
|
|
634
|
+
: `${pendingToolCalls.size} tool ${pendingToolCalls.size === 1 ? 'call' : 'calls'} running…`;
|
|
635
|
+
}
|
|
636
|
+
const streaming = session.state.streamingMessage;
|
|
637
|
+
if (streaming?.role === 'assistant') {
|
|
638
|
+
const response = streaming.content
|
|
639
|
+
.filter((content) => content.type === 'text')
|
|
640
|
+
.map((content) => content.text)
|
|
641
|
+
.join('');
|
|
642
|
+
if (response.trim())
|
|
643
|
+
return activityLine(response);
|
|
644
|
+
}
|
|
645
|
+
return 'thinking…';
|
|
646
|
+
}
|
|
647
|
+
function activityLine(text) {
|
|
648
|
+
return stripTerminalSequences(text)
|
|
649
|
+
.split(/\r?\n/)
|
|
650
|
+
.find((candidate) => candidate.trim())
|
|
651
|
+
?.replace(/[\x00-\x09\x0B-\x1F\x7F]/g, ' ')
|
|
652
|
+
.trim() ?? '';
|
|
653
|
+
}
|
|
654
|
+
function formatStatus(status) {
|
|
655
|
+
return status.replaceAll('_', ' ');
|
|
656
|
+
}
|
|
657
|
+
function formatDuration(record) {
|
|
658
|
+
const start = Date.parse(record.startedAt ?? record.createdAt);
|
|
659
|
+
const end = record.completedAt ? Date.parse(record.completedAt) : Date.now();
|
|
660
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
661
|
+
return '';
|
|
662
|
+
const seconds = Math.max(0, end - start) / 1_000;
|
|
663
|
+
return seconds < 60
|
|
664
|
+
? `${seconds.toFixed(1)}s`
|
|
665
|
+
: `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=agent-navigator.js.map
|