xtralab 0.10.0 → 0.11.1

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.
Files changed (71) hide show
  1. package/README.md +27 -11
  2. package/lib/commandBar/index.d.ts +14 -0
  3. package/lib/commandBar/index.js +114 -0
  4. package/lib/fileBrowser/fileBrowser.js +6 -1
  5. package/lib/highlight/index.d.ts +15 -0
  6. package/lib/highlight/index.js +214 -0
  7. package/lib/index.js +13 -1
  8. package/lib/launcher/agents.d.ts +18 -1
  9. package/lib/launcher/agents.js +47 -4
  10. package/lib/launcher/editorRegistry.js +5 -3
  11. package/lib/launcher/editors.d.ts +17 -2
  12. package/lib/launcher/editors.js +31 -5
  13. package/lib/launcher/index.js +7 -1
  14. package/lib/launcher/schemaDefaults.d.ts +15 -0
  15. package/lib/launcher/schemaDefaults.js +44 -0
  16. package/lib/omnibox/files.d.ts +7 -0
  17. package/lib/omnibox/files.js +50 -0
  18. package/lib/omnibox/index.d.ts +14 -0
  19. package/lib/omnibox/index.js +83 -0
  20. package/lib/omnibox/model.d.ts +45 -0
  21. package/lib/omnibox/model.js +142 -0
  22. package/lib/omnibox/tokens.d.ts +23 -0
  23. package/lib/omnibox/tokens.js +11 -0
  24. package/lib/omnibox/widget.d.ts +36 -0
  25. package/lib/omnibox/widget.js +161 -0
  26. package/lib/searchReplace/index.d.ts +25 -0
  27. package/lib/searchReplace/index.js +68 -0
  28. package/lib/showOutput/index.d.ts +17 -0
  29. package/lib/showOutput/index.js +154 -0
  30. package/lib/terminals/index.d.ts +4 -0
  31. package/lib/terminals/index.js +65 -16
  32. package/lib/terminals/model.d.ts +56 -3
  33. package/lib/terminals/model.js +261 -5
  34. package/lib/terminals/widget.d.ts +3 -1
  35. package/lib/terminals/widget.js +11 -3
  36. package/lib/walkthrough/index.d.ts +14 -0
  37. package/lib/walkthrough/index.js +162 -0
  38. package/lib/walkthrough/panel.d.ts +54 -0
  39. package/lib/walkthrough/panel.js +98 -0
  40. package/package.json +1 -1
  41. package/schema/launcher.json +5 -4
  42. package/schema/terminals.json +15 -0
  43. package/src/commandBar/index.ts +165 -0
  44. package/src/fileBrowser/fileBrowser.tsx +8 -1
  45. package/src/highlight/index.ts +251 -0
  46. package/src/index.ts +13 -1
  47. package/src/launcher/agents.ts +53 -3
  48. package/src/launcher/editorRegistry.ts +8 -4
  49. package/src/launcher/editors.ts +37 -4
  50. package/src/launcher/index.ts +7 -1
  51. package/src/launcher/schemaDefaults.ts +53 -0
  52. package/src/omnibox/files.ts +68 -0
  53. package/src/omnibox/index.ts +105 -0
  54. package/src/omnibox/model.ts +210 -0
  55. package/src/omnibox/tokens.ts +29 -0
  56. package/src/omnibox/widget.tsx +322 -0
  57. package/src/searchReplace/index.ts +86 -0
  58. package/src/showOutput/index.ts +199 -0
  59. package/src/terminals/index.ts +79 -16
  60. package/src/terminals/model.ts +304 -3
  61. package/src/terminals/widget.tsx +21 -4
  62. package/src/walkthrough/index.ts +202 -0
  63. package/src/walkthrough/panel.ts +152 -0
  64. package/style/commandBar.css +104 -0
  65. package/style/highlight.css +11 -0
  66. package/style/index.css +5 -0
  67. package/style/index.js +5 -0
  68. package/style/omnibox.css +120 -0
  69. package/style/showOutput.css +11 -0
  70. package/style/terminals.css +54 -10
  71. package/style/walkthrough.css +80 -0
@@ -17,6 +17,11 @@ export type TerminalWidget = MainAreaWidget<ITerminal.ITerminal>;
17
17
  * xterm-published title) survives the user closing the tab while the
18
18
  * session continues running on the backend.
19
19
  *
20
+ * For sessions running a coding agent it also surfaces a *latest activity*
21
+ * line — the freshest meaningful line of the terminal's output, read from the
22
+ * open tab's xterm buffer on a poll (see {@link activityFor}) — so each row
23
+ * shows what its agent is doing, not just its name.
24
+ *
20
25
  * It also resolves *which agent is running* in each session, so the panel can
21
26
  * badge rows with the agent's logo. Two inputs feed that, reconciled by
22
27
  * {@link agentCommandFor}:
@@ -69,7 +74,10 @@ export declare class SessionRegistry implements IDisposable {
69
74
  */
70
75
  labelFor(name: string): string;
71
76
  /**
72
- * The command of the agent running in the session, or `null` if none.
77
+ * An identifier for the agent running in the session — its configured
78
+ * command, or its canonical id when detection matched the spawned process by
79
+ * id rather than by an aliased command — or `null` if none. Callers resolve
80
+ * it to a logo through the plugin's `iconForCommand`, which accepts either.
73
81
  *
74
82
  * Server-side detection is authoritative whenever it reports a running
75
83
  * agent. A launch tag fills two gaps: the startup grace window right after
@@ -80,6 +88,24 @@ export declare class SessionRegistry implements IDisposable {
80
88
  * exits.
81
89
  */
82
90
  agentCommandFor(name: string): string | null;
91
+ /**
92
+ * The most recent meaningful line of output from the session's terminal, or
93
+ * `null` when there is nothing to surface — no coding agent is running in the
94
+ * session, its tab is closed (so there is no live buffer to read), or the
95
+ * buffer holds only the agent's input box and chrome. Refreshed on a poll by
96
+ * {@link _refreshActivity}; shown as a smaller line under the row's title.
97
+ * Always `null` while the activity line is disabled (see
98
+ * {@link setActivityEnabled}).
99
+ */
100
+ activityFor(name: string): string | null;
101
+ /**
102
+ * Turn the per-row latest-activity line on or off, driven by the
103
+ * `xtralab:terminals` `showAgentActivity` setting. When turned off the
104
+ * activity poll stops reading terminal buffers and any cached lines are
105
+ * dropped, so rows fall back to their title alone; when turned back on the
106
+ * lines are repopulated on the spot. A no-op when the value is unchanged.
107
+ */
108
+ setActivityEnabled(enabled: boolean): void;
83
109
  /**
84
110
  * Return the open widget for a session, if any. The panel uses this to
85
111
  * switch behavior between "activate existing tab" and "open a new tab
@@ -118,6 +144,18 @@ export declare class SessionRegistry implements IDisposable {
118
144
  * clearing them, so a transient error doesn't strip every badge.
119
145
  */
120
146
  private _refreshDetection;
147
+ /**
148
+ * Poll body: re-read the buffer of each open coding-agent terminal and cache
149
+ * its latest meaningful line of output. Only sessions that have a running
150
+ * *agent* (not an editor) and an open tab qualify — a closed tab has no live
151
+ * buffer, editors run full-screen UIs that are not "activity", and plain
152
+ * shells would only echo their prompt. While a tab is reopening (its xterm
153
+ * is not ready yet) any existing line is kept; once the buffer is readable
154
+ * but has nothing worth showing the line is dropped, so a screen the agent
155
+ * has cleared doesn't leave a frozen line behind. Cached lines for sessions
156
+ * that no longer qualify are pruned. Emits only when something changed.
157
+ */
158
+ private _refreshActivity;
121
159
  private _onTagChanged;
122
160
  private _onWidgetAdded;
123
161
  private _onShellCurrentChanged;
@@ -150,12 +188,16 @@ export declare class SessionRegistry implements IDisposable {
150
188
  private _tracker;
151
189
  private _agentSessions;
152
190
  private _detectCommands;
191
+ private _isAgentCommand;
153
192
  private _shell;
154
193
  private _poll;
194
+ private _activityPoll;
155
195
  private _labels;
156
196
  private _ranks;
157
197
  private _firstSeen;
158
198
  private _detected;
199
+ private _activity;
200
+ private _activityEnabled;
159
201
  private _live;
160
202
  private _currentName;
161
203
  private _nextRank;
@@ -183,9 +225,20 @@ export declare namespace SessionRegistry {
183
225
  */
184
226
  agentSessions?: IAgentSessions | null;
185
227
  /**
186
- * Returns the agent commands the server should look for when detecting
187
- * running agents. Read on every poll so it tracks the live agent list.
228
+ * Returns the names the server should look for when detecting running
229
+ * agents — each agent's command together with its canonical id, so a
230
+ * command pointed at an alias (e.g. `ccm` running `claude`) is still
231
+ * matched by the process it spawns. Read on every poll so it tracks the
232
+ * live agent list.
188
233
  */
189
234
  detectCommands?: () => string[];
235
+ /**
236
+ * Whether a detected command (or id) belongs to a coding agent rather than
237
+ * an editor. Only agent sessions get a latest-activity line — editors
238
+ * (Neovim/Vim) are badged too, but run full-screen UIs whose buffer is not
239
+ * meaningfully "activity". Defaults to treating every detected command as an
240
+ * agent.
241
+ */
242
+ isAgentCommand?: (command: string) => boolean;
190
243
  }
191
244
  }
@@ -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._shell = (_c = options.shell) !== null && _c !== void 0 ? _c : null;
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
- (_e = (_d = this._shell) === null || _d === void 0 ? void 0 : _d.currentChanged) === null || _e === void 0 ? void 0 : _e.connect(this._onShellCurrentChanged, this);
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
- (_f = this._agentSessions) === null || _f === void 0 ? void 0 : _f.changed.connect(this._onTagChanged, this);
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
- * The command of the agent running in the session, or `null` if none.
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
@@ -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-label" }, label)),
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;