xtralab 0.10.0 → 0.11.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/README.md +23 -9
- package/lib/commandBar/index.d.ts +14 -0
- package/lib/commandBar/index.js +114 -0
- package/lib/fileBrowser/fileBrowser.js +6 -1
- package/lib/highlight/index.d.ts +15 -0
- package/lib/highlight/index.js +214 -0
- package/lib/index.js +13 -1
- package/lib/launcher/agents.d.ts +7 -0
- package/lib/launcher/agents.js +24 -0
- package/lib/launcher/editorRegistry.js +5 -3
- package/lib/launcher/editors.d.ts +8 -0
- package/lib/launcher/editors.js +17 -0
- package/lib/launcher/index.js +4 -0
- package/lib/launcher/schemaDefaults.d.ts +15 -0
- package/lib/launcher/schemaDefaults.js +44 -0
- package/lib/omnibox/files.d.ts +7 -0
- package/lib/omnibox/files.js +50 -0
- package/lib/omnibox/index.d.ts +14 -0
- package/lib/omnibox/index.js +83 -0
- package/lib/omnibox/model.d.ts +45 -0
- package/lib/omnibox/model.js +142 -0
- package/lib/omnibox/tokens.d.ts +23 -0
- package/lib/omnibox/tokens.js +11 -0
- package/lib/omnibox/widget.d.ts +36 -0
- package/lib/omnibox/widget.js +161 -0
- package/lib/searchReplace/index.d.ts +25 -0
- package/lib/searchReplace/index.js +68 -0
- package/lib/showOutput/index.d.ts +17 -0
- package/lib/showOutput/index.js +154 -0
- package/lib/terminals/index.d.ts +4 -0
- package/lib/terminals/index.js +65 -16
- package/lib/terminals/model.d.ts +56 -3
- package/lib/terminals/model.js +261 -5
- package/lib/terminals/widget.d.ts +3 -1
- package/lib/terminals/widget.js +11 -3
- package/lib/walkthrough/index.d.ts +14 -0
- package/lib/walkthrough/index.js +162 -0
- package/lib/walkthrough/panel.d.ts +54 -0
- package/lib/walkthrough/panel.js +98 -0
- package/package.json +1 -1
- package/schema/launcher.json +3 -2
- package/schema/terminals.json +15 -0
- package/src/commandBar/index.ts +165 -0
- package/src/fileBrowser/fileBrowser.tsx +8 -1
- package/src/highlight/index.ts +251 -0
- package/src/index.ts +13 -1
- package/src/launcher/agents.ts +25 -0
- package/src/launcher/editorRegistry.ts +8 -4
- package/src/launcher/editors.ts +18 -0
- package/src/launcher/index.ts +4 -0
- package/src/launcher/schemaDefaults.ts +53 -0
- package/src/omnibox/files.ts +68 -0
- package/src/omnibox/index.ts +105 -0
- package/src/omnibox/model.ts +210 -0
- package/src/omnibox/tokens.ts +29 -0
- package/src/omnibox/widget.tsx +322 -0
- package/src/searchReplace/index.ts +86 -0
- package/src/showOutput/index.ts +199 -0
- package/src/terminals/index.ts +79 -16
- package/src/terminals/model.ts +304 -3
- package/src/terminals/widget.tsx +21 -4
- package/src/walkthrough/index.ts +202 -0
- package/src/walkthrough/panel.ts +152 -0
- package/style/commandBar.css +104 -0
- package/style/highlight.css +11 -0
- package/style/index.css +5 -0
- package/style/index.js +5 -0
- package/style/omnibox.css +120 -0
- package/style/showOutput.css +11 -0
- package/style/terminals.css +54 -10
- package/style/walkthrough.css +80 -0
package/lib/terminals/model.js
CHANGED
|
@@ -23,6 +23,13 @@ const DETECT_POLL_MAX_MS = 300000;
|
|
|
23
23
|
* for an agent that never really started clears quickly.
|
|
24
24
|
*/
|
|
25
25
|
const LAUNCH_GRACE_MS = 4000;
|
|
26
|
+
/**
|
|
27
|
+
* How often to re-read each open coding-agent terminal's output buffer to
|
|
28
|
+
* refresh the "latest activity" line shown under its row. Brisk enough to feel
|
|
29
|
+
* live while an agent works, cheap because it only walks the last screenful of
|
|
30
|
+
* a handful of terminals and emits only when a line actually changes.
|
|
31
|
+
*/
|
|
32
|
+
const ACTIVITY_POLL_INTERVAL_MS = 1500;
|
|
26
33
|
/**
|
|
27
34
|
* Source-of-truth model for the running-terminals panel. Each running
|
|
28
35
|
* terminal session known to the server is one entry; the registry caches
|
|
@@ -30,6 +37,11 @@ const LAUNCH_GRACE_MS = 4000;
|
|
|
30
37
|
* xterm-published title) survives the user closing the tab while the
|
|
31
38
|
* session continues running on the backend.
|
|
32
39
|
*
|
|
40
|
+
* For sessions running a coding agent it also surfaces a *latest activity*
|
|
41
|
+
* line — the freshest meaningful line of the terminal's output, read from the
|
|
42
|
+
* open tab's xterm buffer on a poll (see {@link activityFor}) — so each row
|
|
43
|
+
* shows what its agent is doing, not just its name.
|
|
44
|
+
*
|
|
33
45
|
* It also resolves *which agent is running* in each session, so the panel can
|
|
34
46
|
* badge rows with the agent's logo. Two inputs feed that, reconciled by
|
|
35
47
|
* {@link agentCommandFor}:
|
|
@@ -52,11 +64,13 @@ const LAUNCH_GRACE_MS = 4000;
|
|
|
52
64
|
*/
|
|
53
65
|
export class SessionRegistry {
|
|
54
66
|
constructor(options) {
|
|
55
|
-
var _a, _b, _c, _d, _e, _f;
|
|
67
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
56
68
|
this._labels = new Map();
|
|
57
69
|
this._ranks = new Map();
|
|
58
70
|
this._firstSeen = new Map();
|
|
59
71
|
this._detected = new Map();
|
|
72
|
+
this._activity = new Map();
|
|
73
|
+
this._activityEnabled = true;
|
|
60
74
|
this._live = new Set();
|
|
61
75
|
this._currentName = null;
|
|
62
76
|
this._nextRank = 100;
|
|
@@ -66,7 +80,8 @@ export class SessionRegistry {
|
|
|
66
80
|
this._tracker = options.tracker;
|
|
67
81
|
this._agentSessions = (_a = options.agentSessions) !== null && _a !== void 0 ? _a : null;
|
|
68
82
|
this._detectCommands = (_b = options.detectCommands) !== null && _b !== void 0 ? _b : (() => []);
|
|
69
|
-
this.
|
|
83
|
+
this._isAgentCommand = (_c = options.isAgentCommand) !== null && _c !== void 0 ? _c : (() => true);
|
|
84
|
+
this._shell = (_d = options.shell) !== null && _d !== void 0 ? _d : null;
|
|
70
85
|
this._terminals.runningChanged.connect(this._onRunningChanged, this);
|
|
71
86
|
this._tracker.widgetAdded.connect(this._onWidgetAdded, this);
|
|
72
87
|
this._tracker.forEach(widget => this._trackWidget(widget));
|
|
@@ -76,13 +91,13 @@ export class SessionRegistry {
|
|
|
76
91
|
// other non-terminal tab is current instead. `currentChanged` is optional
|
|
77
92
|
// on the shell interface (not every shell can switch focus), so guard it;
|
|
78
93
|
// when it is absent the highlight stays off.
|
|
79
|
-
(
|
|
94
|
+
(_f = (_e = this._shell) === null || _e === void 0 ? void 0 : _e.currentChanged) === null || _f === void 0 ? void 0 : _f.connect(this._onShellCurrentChanged, this);
|
|
80
95
|
this._updateCurrent();
|
|
81
96
|
// A freshly written launch tag should re-render the panel immediately
|
|
82
97
|
// (show the logo) and again once the grace window closes (so a tag for
|
|
83
98
|
// an agent that never started, or that detection later contradicts,
|
|
84
99
|
// doesn't linger).
|
|
85
|
-
(
|
|
100
|
+
(_g = this._agentSessions) === null || _g === void 0 ? void 0 : _g.changed.connect(this._onTagChanged, this);
|
|
86
101
|
this._poll = new Poll({
|
|
87
102
|
name: '@xtralab/terminals:runningAgents',
|
|
88
103
|
factory: () => this._refreshDetection(),
|
|
@@ -93,6 +108,16 @@ export class SessionRegistry {
|
|
|
93
108
|
},
|
|
94
109
|
standby: 'when-hidden'
|
|
95
110
|
});
|
|
111
|
+
// A second, faster poll reads the live output buffer of each open
|
|
112
|
+
// coding-agent terminal to keep its "latest activity" line current. Kept
|
|
113
|
+
// separate from the detection poll so reading xterm buffers neither delays
|
|
114
|
+
// nor is delayed by the server round-trip that drives the agent badges.
|
|
115
|
+
this._activityPoll = new Poll({
|
|
116
|
+
name: '@xtralab/terminals:activity',
|
|
117
|
+
factory: () => this._refreshActivity(),
|
|
118
|
+
frequency: { interval: ACTIVITY_POLL_INTERVAL_MS, backoff: false },
|
|
119
|
+
standby: 'when-hidden'
|
|
120
|
+
});
|
|
96
121
|
}
|
|
97
122
|
/**
|
|
98
123
|
* Emitted whenever the live session list, a cached label, or the detected
|
|
@@ -144,7 +169,10 @@ export class SessionRegistry {
|
|
|
144
169
|
return `Terminal ${name}`;
|
|
145
170
|
}
|
|
146
171
|
/**
|
|
147
|
-
*
|
|
172
|
+
* An identifier for the agent running in the session — its configured
|
|
173
|
+
* command, or its canonical id when detection matched the spawned process by
|
|
174
|
+
* id rather than by an aliased command — or `null` if none. Callers resolve
|
|
175
|
+
* it to a logo through the plugin's `iconForCommand`, which accepts either.
|
|
148
176
|
*
|
|
149
177
|
* Server-side detection is authoritative whenever it reports a running
|
|
150
178
|
* agent. A launch tag fills two gaps: the startup grace window right after
|
|
@@ -174,6 +202,44 @@ export class SessionRegistry {
|
|
|
174
202
|
}
|
|
175
203
|
return null;
|
|
176
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* The most recent meaningful line of output from the session's terminal, or
|
|
207
|
+
* `null` when there is nothing to surface — no coding agent is running in the
|
|
208
|
+
* session, its tab is closed (so there is no live buffer to read), or the
|
|
209
|
+
* buffer holds only the agent's input box and chrome. Refreshed on a poll by
|
|
210
|
+
* {@link _refreshActivity}; shown as a smaller line under the row's title.
|
|
211
|
+
* Always `null` while the activity line is disabled (see
|
|
212
|
+
* {@link setActivityEnabled}).
|
|
213
|
+
*/
|
|
214
|
+
activityFor(name) {
|
|
215
|
+
var _a;
|
|
216
|
+
if (!this._activityEnabled) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return (_a = this._activity.get(name)) !== null && _a !== void 0 ? _a : null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Turn the per-row latest-activity line on or off, driven by the
|
|
223
|
+
* `xtralab:terminals` `showAgentActivity` setting. When turned off the
|
|
224
|
+
* activity poll stops reading terminal buffers and any cached lines are
|
|
225
|
+
* dropped, so rows fall back to their title alone; when turned back on the
|
|
226
|
+
* lines are repopulated on the spot. A no-op when the value is unchanged.
|
|
227
|
+
*/
|
|
228
|
+
setActivityEnabled(enabled) {
|
|
229
|
+
if (enabled === this._activityEnabled) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this._activityEnabled = enabled;
|
|
233
|
+
if (enabled) {
|
|
234
|
+
// Repopulate immediately rather than waiting for the next poll tick;
|
|
235
|
+
// `_refreshActivity` emits `stateChanged` if it finds anything to show.
|
|
236
|
+
void this._refreshActivity();
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
this._activity.clear();
|
|
240
|
+
this._stateChanged.emit();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
177
243
|
/**
|
|
178
244
|
* Return the open widget for a session, if any. The panel uses this to
|
|
179
245
|
* switch behavior between "activate existing tab" and "open a new tab
|
|
@@ -215,6 +281,7 @@ export class SessionRegistry {
|
|
|
215
281
|
}
|
|
216
282
|
this._isDisposed = true;
|
|
217
283
|
this._poll.dispose();
|
|
284
|
+
this._activityPoll.dispose();
|
|
218
285
|
this._terminals.runningChanged.disconnect(this._onRunningChanged, this);
|
|
219
286
|
this._tracker.widgetAdded.disconnect(this._onWidgetAdded, this);
|
|
220
287
|
(_a = this._agentSessions) === null || _a === void 0 ? void 0 : _a.changed.disconnect(this._onTagChanged, this);
|
|
@@ -272,6 +339,11 @@ export class SessionRegistry {
|
|
|
272
339
|
this._detected.delete(name);
|
|
273
340
|
}
|
|
274
341
|
}
|
|
342
|
+
for (const name of Array.from(this._activity.keys())) {
|
|
343
|
+
if (!next.has(name)) {
|
|
344
|
+
this._activity.delete(name);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
275
347
|
// Forget launch tags for sessions that have gone away. This matters for
|
|
276
348
|
// correctness as well as bookkeeping: terminado reuses session names, so
|
|
277
349
|
// a stale tag could otherwise mislabel a brand-new terminal that happens
|
|
@@ -311,6 +383,62 @@ export class SessionRegistry {
|
|
|
311
383
|
this._stateChanged.emit();
|
|
312
384
|
}
|
|
313
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Poll body: re-read the buffer of each open coding-agent terminal and cache
|
|
388
|
+
* its latest meaningful line of output. Only sessions that have a running
|
|
389
|
+
* *agent* (not an editor) and an open tab qualify — a closed tab has no live
|
|
390
|
+
* buffer, editors run full-screen UIs that are not "activity", and plain
|
|
391
|
+
* shells would only echo their prompt. While a tab is reopening (its xterm
|
|
392
|
+
* is not ready yet) any existing line is kept; once the buffer is readable
|
|
393
|
+
* but has nothing worth showing the line is dropped, so a screen the agent
|
|
394
|
+
* has cleared doesn't leave a frozen line behind. Cached lines for sessions
|
|
395
|
+
* that no longer qualify are pruned. Emits only when something changed.
|
|
396
|
+
*/
|
|
397
|
+
async _refreshActivity() {
|
|
398
|
+
if (!this._activityEnabled) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
let changed = false;
|
|
402
|
+
const qualifying = new Set();
|
|
403
|
+
for (const name of this._live) {
|
|
404
|
+
const command = this.agentCommandFor(name);
|
|
405
|
+
if (command === null || !this._isAgentCommand(command)) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const widget = this.widgetFor(name);
|
|
409
|
+
if (!widget) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
qualifying.add(name);
|
|
413
|
+
const term = widget.content._term;
|
|
414
|
+
if (!term) {
|
|
415
|
+
// The tab is reopening and its xterm is not ready; keep any existing
|
|
416
|
+
// line until the buffer can be read again.
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const line = Private.readActivity(term);
|
|
420
|
+
if (line) {
|
|
421
|
+
if (this._activity.get(name) !== line) {
|
|
422
|
+
this._activity.set(name, line);
|
|
423
|
+
changed = true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else if (this._activity.delete(name)) {
|
|
427
|
+
// Readable buffer with nothing to surface (e.g. the agent cleared its
|
|
428
|
+
// screen) — drop the now-stale line instead of freezing it.
|
|
429
|
+
changed = true;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const name of Array.from(this._activity.keys())) {
|
|
433
|
+
if (!qualifying.has(name)) {
|
|
434
|
+
this._activity.delete(name);
|
|
435
|
+
changed = true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (changed) {
|
|
439
|
+
this._stateChanged.emit();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
314
442
|
_onTagChanged() {
|
|
315
443
|
this._stateChanged.emit();
|
|
316
444
|
// Re-render once the grace window closes so a tag that detection never
|
|
@@ -415,4 +543,132 @@ var Private;
|
|
|
415
543
|
return true;
|
|
416
544
|
}
|
|
417
545
|
Private.detectedEqual = detectedEqual;
|
|
546
|
+
/** Rows above the cursor to scan when looking for the latest output. */
|
|
547
|
+
const ACTIVITY_SCAN_ROWS = 64;
|
|
548
|
+
/** Clamp for the activity string so one runaway block can't bloat a row. */
|
|
549
|
+
const ACTIVITY_MAX_LENGTH = 160;
|
|
550
|
+
/**
|
|
551
|
+
* Horizontal box-drawing / rule characters, treated as blanks when
|
|
552
|
+
* sanitizing. Agents pad a status line or draw a separator with these (e.g.
|
|
553
|
+
* `Worked for 1m 06s ────`), which is chrome, not text. Vertical bars are
|
|
554
|
+
* deliberately excluded — {@link isMeaningfulActivity} relies on them to
|
|
555
|
+
* recognise (and skip) the contents of an input/quote box.
|
|
556
|
+
*/
|
|
557
|
+
const RULE_CHARS = new Set([
|
|
558
|
+
0x2500, 0x2501, 0x2504, 0x2505, 0x2508, 0x2509, 0x254c, 0x254d, 0x2550
|
|
559
|
+
]);
|
|
560
|
+
/**
|
|
561
|
+
* The most recent meaningful line of agent output, or `null` if none is
|
|
562
|
+
* found. Agents park the cursor in an input box at the bottom of the screen,
|
|
563
|
+
* so the scan starts there and walks *up*, skipping that box, any hint or
|
|
564
|
+
* footer beneath it, blank lines and separators, until it reaches the block
|
|
565
|
+
* of real output nearest the input. It then rewinds to that block's first row
|
|
566
|
+
* and stitches the block back together (see {@link joinBlock}), so a reply
|
|
567
|
+
* the agent hard-wrapped across several rows reads as its opening sentence
|
|
568
|
+
* rather than the trailing fragment left next to the cursor.
|
|
569
|
+
*/
|
|
570
|
+
function readActivity(term) {
|
|
571
|
+
var _a;
|
|
572
|
+
const buffer = (_a = term.buffer) === null || _a === void 0 ? void 0 : _a.active;
|
|
573
|
+
if (!buffer) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
const cursorRow = buffer.baseY + buffer.cursorY;
|
|
577
|
+
const limit = Math.max(0, cursorRow - ACTIVITY_SCAN_ROWS);
|
|
578
|
+
for (let row = cursorRow; row >= limit; row--) {
|
|
579
|
+
if (!isMeaningfulActivity(lineText(buffer, row))) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
// `row` is the bottom of the output block nearest the input; walk up
|
|
583
|
+
// while rows stay meaningful to find the block's first row.
|
|
584
|
+
let topRow = row;
|
|
585
|
+
while (topRow > limit &&
|
|
586
|
+
isMeaningfulActivity(lineText(buffer, topRow - 1))) {
|
|
587
|
+
topRow--;
|
|
588
|
+
}
|
|
589
|
+
return clampActivity(joinBlock(buffer, topRow, row));
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
Private.readActivity = readActivity;
|
|
594
|
+
/** Sanitized text of one buffer row. */
|
|
595
|
+
function lineText(buffer, row) {
|
|
596
|
+
var _a, _b;
|
|
597
|
+
return sanitizeActivity((_b = (_a = buffer.getLine(row)) === null || _a === void 0 ? void 0 : _a.translateToString(true)) !== null && _b !== void 0 ? _b : '');
|
|
598
|
+
}
|
|
599
|
+
Private.lineText = lineText;
|
|
600
|
+
/**
|
|
601
|
+
* Stitch rows `[topRow, lastRow]` of one output block into a single string,
|
|
602
|
+
* stopping once it is long enough to fill the row. A soft-wrapped row
|
|
603
|
+
* continues the previous one mid-word, so it is appended directly; a hard
|
|
604
|
+
* newline is a word boundary, so it is joined with a space.
|
|
605
|
+
*/
|
|
606
|
+
function joinBlock(buffer, topRow, lastRow) {
|
|
607
|
+
var _a;
|
|
608
|
+
let result = lineText(buffer, topRow);
|
|
609
|
+
for (let row = topRow + 1; row <= lastRow; row++) {
|
|
610
|
+
if (Array.from(result).length >= ACTIVITY_MAX_LENGTH) {
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
const bufferLine = buffer.getLine(row);
|
|
614
|
+
const text = sanitizeActivity((_a = bufferLine === null || bufferLine === void 0 ? void 0 : bufferLine.translateToString(true)) !== null && _a !== void 0 ? _a : '');
|
|
615
|
+
if (!text) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
result += (bufferLine === null || bufferLine === void 0 ? void 0 : bufferLine.isWrapped) ? text : ` ${text}`;
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
Private.joinBlock = joinBlock;
|
|
623
|
+
/** Truncate by code point (never splitting an emoji) with an ellipsis. */
|
|
624
|
+
function clampActivity(text) {
|
|
625
|
+
const points = Array.from(text);
|
|
626
|
+
return points.length > ACTIVITY_MAX_LENGTH
|
|
627
|
+
? `${points.slice(0, ACTIVITY_MAX_LENGTH - 1).join('')}…`
|
|
628
|
+
: text;
|
|
629
|
+
}
|
|
630
|
+
Private.clampActivity = clampActivity;
|
|
631
|
+
/**
|
|
632
|
+
* Collapse a raw buffer row into displayable text: replace control characters
|
|
633
|
+
* and horizontal rule characters with spaces (so no escape sequence or drawn
|
|
634
|
+
* separator rides along), squeeze runs of whitespace, and trim.
|
|
635
|
+
*/
|
|
636
|
+
function sanitizeActivity(value) {
|
|
637
|
+
var _a;
|
|
638
|
+
let result = '';
|
|
639
|
+
for (const char of value) {
|
|
640
|
+
const code = (_a = char.codePointAt(0)) !== null && _a !== void 0 ? _a : 0;
|
|
641
|
+
const isControl = code < 0x20 || (code >= 0x7f && code <= 0x9f);
|
|
642
|
+
result += isControl || RULE_CHARS.has(code) ? ' ' : char;
|
|
643
|
+
}
|
|
644
|
+
return result.replace(/\s+/g, ' ').trim();
|
|
645
|
+
}
|
|
646
|
+
Private.sanitizeActivity = sanitizeActivity;
|
|
647
|
+
/**
|
|
648
|
+
* Whether a sanitized row is worth showing as activity, i.e. real output
|
|
649
|
+
* rather than chrome. Doubles as the block-boundary test for
|
|
650
|
+
* {@link readActivity}'s walk up. A row is skipped when it:
|
|
651
|
+
* - is blank or pure box-drawing (carries no letter or digit);
|
|
652
|
+
* - begins with a vertical box-drawing bar (the contents of an input or
|
|
653
|
+
* quote box), a prompt chevron (the input row and its ghost suggestion),
|
|
654
|
+
* or Claude's tool-result / tip marker `⎿`; or
|
|
655
|
+
* - is a transient status footer — one carrying a `for <time>` / `(<time>`
|
|
656
|
+
* elapsed-time stamp, as agents print while and after they work
|
|
657
|
+
* ("✻ Churned for 17s", "Working… (8s · …)", "— Worked for 1m 06s"). This
|
|
658
|
+
* carries no real content, so skipping it lets the scan reach the reply
|
|
659
|
+
* the agent wrote just above it.
|
|
660
|
+
*/
|
|
661
|
+
function isMeaningfulActivity(text) {
|
|
662
|
+
if (!text) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (/^[│┃║❯❮›‹⎿]/u.test(text)) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (/(?:for |\()\d+(?:\s?m\s?\d+)?s\b/u.test(text)) {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
return /[\p{L}\p{N}]/u.test(text);
|
|
672
|
+
}
|
|
673
|
+
Private.isMeaningfulActivity = isMeaningfulActivity;
|
|
418
674
|
})(Private || (Private = {}));
|
|
@@ -13,7 +13,9 @@ export declare const RUNNING_TERMINALS_ID = "xtralab-running-terminals";
|
|
|
13
13
|
* launcher *starts* sessions, this panel surfaces the ones already
|
|
14
14
|
* running so the user can jump back to a backgrounded agent — including
|
|
15
15
|
* sessions whose tab has been closed but which are still alive on the
|
|
16
|
-
* server.
|
|
16
|
+
* server. Each row is labelled with the session's title and, for a session
|
|
17
|
+
* running a coding agent, a smaller line below it showing the agent's latest
|
|
18
|
+
* line of output (from {@link SessionRegistry.activityFor}).
|
|
17
19
|
*
|
|
18
20
|
* Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
|
|
19
21
|
* whenever the registry emits `stateChanged`, so it tracks
|
package/lib/terminals/widget.js
CHANGED
|
@@ -12,7 +12,9 @@ export const RUNNING_TERMINALS_ID = 'xtralab-running-terminals';
|
|
|
12
12
|
* launcher *starts* sessions, this panel surfaces the ones already
|
|
13
13
|
* running so the user can jump back to a backgrounded agent — including
|
|
14
14
|
* sessions whose tab has been closed but which are still alive on the
|
|
15
|
-
* server.
|
|
15
|
+
* server. Each row is labelled with the session's title and, for a session
|
|
16
|
+
* running a coding agent, a smaller line below it showing the agent's latest
|
|
17
|
+
* line of output (from {@link SessionRegistry.activityFor}).
|
|
16
18
|
*
|
|
17
19
|
* Backed by {@link SessionRegistry}: a `UseSignal` re-renders the list
|
|
18
20
|
* whenever the registry emits `stateChanged`, so it tracks
|
|
@@ -81,12 +83,18 @@ function RunningTerminalsComponent(props) {
|
|
|
81
83
|
// icon when nothing recognised is running in the session.
|
|
82
84
|
const RowIcon = iconForCommand(registry.agentCommandFor(name)).react;
|
|
83
85
|
const isCurrent = name === currentName;
|
|
86
|
+
// The latest line of output from the session's agent, shown as a
|
|
87
|
+
// smaller line under the title; `null` for rows with nothing to
|
|
88
|
+
// surface (no agent running, or no open tab to read a live buffer).
|
|
89
|
+
const activity = registry.activityFor(name);
|
|
84
90
|
return (React.createElement("li", { key: name, className: isCurrent
|
|
85
91
|
? 'jp-xtralab-Terminals-item jp-mod-current'
|
|
86
92
|
: 'jp-xtralab-Terminals-item' },
|
|
87
93
|
React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-activate", onClick: () => onActivate(name), title: tooltip, "aria-label": tooltip, "aria-current": isCurrent || undefined },
|
|
88
|
-
React.createElement(RowIcon, { tag: "span", verticalAlign: "middle" }),
|
|
89
|
-
React.createElement("span", { className: "jp-xtralab-Terminals-item-
|
|
94
|
+
React.createElement(RowIcon, { tag: "span", className: "jp-xtralab-Terminals-item-icon", verticalAlign: "middle" }),
|
|
95
|
+
React.createElement("span", { className: "jp-xtralab-Terminals-item-text" },
|
|
96
|
+
React.createElement("span", { className: "jp-xtralab-Terminals-item-label" }, label),
|
|
97
|
+
activity ? (React.createElement("span", { className: "jp-xtralab-Terminals-item-detail" }, activity)) : null)),
|
|
90
98
|
React.createElement("button", { type: "button", className: "jp-xtralab-Terminals-item-close", onClick: () => onShutdown(name), title: trans.__('Shut down %1', label), "aria-label": trans.__('Shut down %1', label) },
|
|
91
99
|
React.createElement(closeIcon.react, { tag: "span", verticalAlign: "middle" }))));
|
|
92
100
|
})))));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
|
+
/**
|
|
3
|
+
* Contribute `xtralab:walkthrough`: build a persistent, read-only walkthrough
|
|
4
|
+
* in the right side area instead of narrating only in the agent's chat.
|
|
5
|
+
*
|
|
6
|
+
* Each call appends a step (Markdown prose, an optional embedded visual, and an
|
|
7
|
+
* optional code reference). When a step names a file, the editor follows along
|
|
8
|
+
* (opening it full-width in the main area, no split) and its lines are
|
|
9
|
+
* highlighted; the step also keeps a button so the user can jump back to it
|
|
10
|
+
* later. The panel accumulates the whole tour beside the code, so the user can
|
|
11
|
+
* read at their own pace rather than chasing the chat.
|
|
12
|
+
*/
|
|
13
|
+
declare const plugin: JupyterFrontEndPlugin<void>;
|
|
14
|
+
export default plugin;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ILabShell } from '@jupyterlab/application';
|
|
2
|
+
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
3
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
5
|
+
import { tocIcon } from '@jupyterlab/ui-components';
|
|
6
|
+
import { WalkthroughPanel } from './panel';
|
|
7
|
+
const PLUGIN_ID = 'xtralab:walkthrough';
|
|
8
|
+
/** Append a step to the read-only walkthrough panel. */
|
|
9
|
+
const WALKTHROUGH_COMMAND = 'xtralab:walkthrough';
|
|
10
|
+
const HIGHLIGHT_LINES_COMMAND = 'xtralab:highlight-lines';
|
|
11
|
+
const PANEL_ID = 'xtralab-walkthrough';
|
|
12
|
+
/** Pull a `{ mimeType, data }` media object out of a JSON command argument. */
|
|
13
|
+
function readMedia(value) {
|
|
14
|
+
if (value &&
|
|
15
|
+
typeof value === 'object' &&
|
|
16
|
+
!Array.isArray(value) &&
|
|
17
|
+
typeof value.mimeType === 'string') {
|
|
18
|
+
const media = value;
|
|
19
|
+
return { mimeType: media.mimeType, data: media.data };
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
function toLine(value) {
|
|
24
|
+
const n = Math.trunc(Number(value));
|
|
25
|
+
return Number.isFinite(n) && n >= 1 ? n : undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Contribute `xtralab:walkthrough`: build a persistent, read-only walkthrough
|
|
29
|
+
* in the right side area instead of narrating only in the agent's chat.
|
|
30
|
+
*
|
|
31
|
+
* Each call appends a step (Markdown prose, an optional embedded visual, and an
|
|
32
|
+
* optional code reference). When a step names a file, the editor follows along
|
|
33
|
+
* (opening it full-width in the main area, no split) and its lines are
|
|
34
|
+
* highlighted; the step also keeps a button so the user can jump back to it
|
|
35
|
+
* later. The panel accumulates the whole tour beside the code, so the user can
|
|
36
|
+
* read at their own pace rather than chasing the chat.
|
|
37
|
+
*/
|
|
38
|
+
const plugin = {
|
|
39
|
+
id: PLUGIN_ID,
|
|
40
|
+
description: 'Build a read-only guided walkthrough in the side panel.',
|
|
41
|
+
autoStart: true,
|
|
42
|
+
requires: [IRenderMimeRegistry, ILabShell],
|
|
43
|
+
optional: [ICommandPalette, ITranslator],
|
|
44
|
+
activate: (app, rendermime, labShell, palette, translator) => {
|
|
45
|
+
const { commands } = app;
|
|
46
|
+
const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
|
|
47
|
+
let panel = null;
|
|
48
|
+
const ensurePanel = () => {
|
|
49
|
+
if (!panel || panel.isDisposed) {
|
|
50
|
+
panel = new WalkthroughPanel({ rendermime, commands, trans });
|
|
51
|
+
panel.id = PANEL_ID;
|
|
52
|
+
panel.title.icon = tocIcon;
|
|
53
|
+
panel.title.label = trans.__('Walkthrough');
|
|
54
|
+
panel.title.caption = trans.__('Walkthrough');
|
|
55
|
+
// Closable so it gets a close button if the user drags it out of the
|
|
56
|
+
// side area into the main document area. Closing disposes it; the next
|
|
57
|
+
// xtralab:walkthrough call recreates it back in the side area.
|
|
58
|
+
panel.title.closable = true;
|
|
59
|
+
labShell.add(panel, 'right', { rank: 1000, type: 'Walkthrough' });
|
|
60
|
+
const created = panel;
|
|
61
|
+
created.disposed.connect(() => {
|
|
62
|
+
if (panel === created) {
|
|
63
|
+
panel = null;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return panel;
|
|
68
|
+
};
|
|
69
|
+
commands.addCommand(WALKTHROUGH_COMMAND, {
|
|
70
|
+
label: trans.__('Add Walkthrough Step'),
|
|
71
|
+
caption: trans.__('Append a step to the read-only walkthrough panel'),
|
|
72
|
+
describedBy: {
|
|
73
|
+
args: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
title: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'Optional heading for the step.'
|
|
79
|
+
},
|
|
80
|
+
body: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Markdown for the step. Renders code snippets, lists, math, and Mermaid diagrams from a ```mermaid fenced block.'
|
|
83
|
+
},
|
|
84
|
+
path: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Optional file the step is about, relative to the server root. The editor opens it (full width, no split) and highlights the lines, and the step keeps a button to jump back.'
|
|
87
|
+
},
|
|
88
|
+
line: {
|
|
89
|
+
type: 'number',
|
|
90
|
+
description: 'First line to highlight (1-indexed).'
|
|
91
|
+
},
|
|
92
|
+
endLine: {
|
|
93
|
+
type: 'number',
|
|
94
|
+
description: 'Last line to highlight (1-indexed, inclusive).'
|
|
95
|
+
},
|
|
96
|
+
media: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
description: 'Optional embedded visual, e.g. { "mimeType": "application/vnd.vegalite.v5+json", "data": <spec> }.',
|
|
99
|
+
properties: {
|
|
100
|
+
mimeType: { type: 'string' },
|
|
101
|
+
data: {}
|
|
102
|
+
},
|
|
103
|
+
required: ['mimeType', 'data']
|
|
104
|
+
},
|
|
105
|
+
reveal: {
|
|
106
|
+
type: 'boolean',
|
|
107
|
+
description: 'Whether to navigate and highlight the code as the step is added. Defaults to true.'
|
|
108
|
+
},
|
|
109
|
+
reset: {
|
|
110
|
+
type: 'boolean',
|
|
111
|
+
description: 'Clear the existing walkthrough before adding this step (start a new tour).'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
execute: async (args) => {
|
|
117
|
+
const current = ensurePanel();
|
|
118
|
+
if (args['reset'] === true) {
|
|
119
|
+
current.clear();
|
|
120
|
+
}
|
|
121
|
+
const path = typeof args['path'] === 'string' ? args['path'] : undefined;
|
|
122
|
+
const line = toLine(args['line']);
|
|
123
|
+
const endLine = toLine(args['endLine']);
|
|
124
|
+
const media = readMedia(args['media']);
|
|
125
|
+
const step = {
|
|
126
|
+
title: typeof args['title'] === 'string' ? args['title'] : undefined,
|
|
127
|
+
body: typeof args['body'] === 'string' ? args['body'] : undefined,
|
|
128
|
+
media,
|
|
129
|
+
path,
|
|
130
|
+
line,
|
|
131
|
+
endLine
|
|
132
|
+
};
|
|
133
|
+
if (!step.title &&
|
|
134
|
+
!step.body &&
|
|
135
|
+
!step.media &&
|
|
136
|
+
!step.path &&
|
|
137
|
+
args['reset'] !== true) {
|
|
138
|
+
throw new Error('xtralab:walkthrough needs at least one of: title, body, media, path');
|
|
139
|
+
}
|
|
140
|
+
// Reveal the panel, then render the step into it.
|
|
141
|
+
labShell.activateById(current.id);
|
|
142
|
+
await current.addStep(step);
|
|
143
|
+
// Let the editor follow the newest step.
|
|
144
|
+
if (path && args['reveal'] !== false) {
|
|
145
|
+
await commands.execute(HIGHLIGHT_LINES_COMMAND, {
|
|
146
|
+
path,
|
|
147
|
+
line,
|
|
148
|
+
endLine
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return trans.__('Added walkthrough step');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (palette) {
|
|
155
|
+
palette.addItem({
|
|
156
|
+
command: WALKTHROUGH_COMMAND,
|
|
157
|
+
category: trans.__('Other')
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
export default plugin;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
2
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
3
|
+
import type { CommandRegistry } from '@lumino/commands';
|
|
4
|
+
import type { ReadonlyPartialJSONValue } from '@lumino/coreutils';
|
|
5
|
+
import type { Message } from '@lumino/messaging';
|
|
6
|
+
import { Panel } from '@lumino/widgets';
|
|
7
|
+
/** A rich visual embedded in a step (rendered through the rendermime registry). */
|
|
8
|
+
export interface IWalkthroughMedia {
|
|
9
|
+
mimeType: string;
|
|
10
|
+
data: ReadonlyPartialJSONValue;
|
|
11
|
+
}
|
|
12
|
+
/** One step of a walkthrough. Every field is optional, but a step needs at least one. */
|
|
13
|
+
export interface IWalkthroughStep {
|
|
14
|
+
title?: string;
|
|
15
|
+
body?: string;
|
|
16
|
+
media?: IWalkthroughMedia;
|
|
17
|
+
path?: string;
|
|
18
|
+
line?: number;
|
|
19
|
+
endLine?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare namespace WalkthroughPanel {
|
|
22
|
+
interface IOptions {
|
|
23
|
+
rendermime: IRenderMimeRegistry;
|
|
24
|
+
commands: CommandRegistry;
|
|
25
|
+
trans: TranslationBundle;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A read-only, scrollable column of walkthrough steps for the side area.
|
|
30
|
+
*
|
|
31
|
+
* The agent appends steps as it narrates, so the explanation lives beside the
|
|
32
|
+
* code the user is looking at and persists for them to read at their own pace,
|
|
33
|
+
* rather than scrolling away in the chat. Each step renders Markdown (with code
|
|
34
|
+
* snippets, Mermaid, math), can embed a rich visual, and can offer a button
|
|
35
|
+
* that opens the referenced file and highlights its lines.
|
|
36
|
+
*/
|
|
37
|
+
export declare class WalkthroughPanel extends Panel {
|
|
38
|
+
constructor(options: WalkthroughPanel.IOptions);
|
|
39
|
+
/** Remove every step, leaving an empty panel. */
|
|
40
|
+
clear(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Dispose on close (e.g. the close button when the panel has been dragged to
|
|
43
|
+
* the main area) rather than leaving a detached, reusable widget behind. The
|
|
44
|
+
* next `xtralab:walkthrough` call recreates the panel in the side area.
|
|
45
|
+
*/
|
|
46
|
+
protected onCloseRequest(msg: Message): void;
|
|
47
|
+
/** Append a step and scroll it into view. */
|
|
48
|
+
addStep(step: IWalkthroughStep): Promise<void>;
|
|
49
|
+
private _render;
|
|
50
|
+
private _jumpButton;
|
|
51
|
+
private _rendermime;
|
|
52
|
+
private _commands;
|
|
53
|
+
private _trans;
|
|
54
|
+
}
|