crewforth 3.1.0-rc.2 → 3.1.0-rc.4

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.
@@ -11,9 +11,11 @@
11
11
  // A status is never colour alone: every dot has its word beside it.
12
12
  // A poll that changed nothing writes nothing. Each element remembers what it last drew.
13
13
  import {
14
- plan, sequence, crowdFolds, fitZoom, stateOf, ZOOM, GROUPINGS, DENSITIES,
14
+ plan, sequence, crowdFolds, fitZoom, stateOf, ZOOM, GROUPINGS, DENSITIES, INFERRED_NOTE,
15
15
  } from './graph-plan.js';
16
+ import { clock } from './timeline-plan.js';
16
17
  import { agentStatus } from './nav.js';
18
+ import { roleName, shownType, hoverOf, orderTag } from './names.js';
17
19
 
18
20
  // How long an arriving wire takes to draw itself toward its new card. Short on purpose: this is a status panel,
19
21
  // and anything a viewer has to wait through is a cost they pay on every spawn.
@@ -84,6 +86,7 @@ export class Canvas {
84
86
  this.mapRects = new Map(); // drawn item id -> its box on the minimap
85
87
  this.seq = new Map(); // id -> arrival number (see graph-plan.js)
86
88
  this.open = new Map(); // group id -> the viewer's own fold
89
+ this.aside = false; // an agent group opens into the next column, not downward
87
90
  // Ids that arrived on the last poll. A wire into one of these draws itself; every other wire is left alone,
88
91
  // so opening a group of 105 does not set 105 animations running at once.
89
92
  this.newborn = new Set();
@@ -139,11 +142,53 @@ export class Canvas {
139
142
  this.mapEl.append(this.mapSvg);
140
143
 
141
144
  this.legendEl = this.#legend();
145
+ this.spendEl = this.#spend();
142
146
  this.tipEl = mk('div', 'cv-tip');
143
147
  this.tipEl.setAttribute('role', 'tooltip');
144
148
  this.tipEl.hidden = true;
145
149
 
146
- this.root.append(this.viewport, this.emptyEl, this.mapEl, this.legendEl, this.tipEl);
150
+ this.root.append(this.viewport, this.emptyEl, this.mapEl, this.legendEl, this.spendEl, this.tipEl);
151
+ }
152
+
153
+ /** What the session spent: time, tokens and an estimate of the cost. Closed, it stays closed until asked for. */
154
+ #spend() {
155
+ const box = mk('div', 'cv-legend cv-spend');
156
+ box.setAttribute('aria-label', 'Session summary');
157
+ const head = mk('div', 'cv-legend-head');
158
+ head.append(mk('span', 'cv-label', 'Session'));
159
+ const close = mk('button', 'cv-x');
160
+ close.type = 'button';
161
+ close.setAttribute('aria-label', 'Close the session summary');
162
+ close.innerHTML = MARK.close;
163
+ close.addEventListener('click', () => this.showSpend(false));
164
+ head.append(close);
165
+ this.spendRows = mk('dl', 'cv-spend-rows');
166
+ box.append(head, this.spendRows);
167
+ this.spendOpen = readFlag('crewforth-studio-spend') !== 'closed';
168
+ box.hidden = true; // until there is a session's usage to show
169
+ return box;
170
+ }
171
+
172
+ showSpend(on) {
173
+ this.spendOpen = Boolean(on);
174
+ writeFlag('crewforth-studio-spend', on ? 'open' : 'closed');
175
+ this.spendEl.hidden = !on || !this.spendCount;
176
+ }
177
+
178
+ /** The rows to show: [label, value, note], from usage.js `summaryRows`. An empty list hides the box. */
179
+ setSpend(rows) {
180
+ const sig = JSON.stringify(rows ?? []);
181
+ if (sig === this.spendSig) return;
182
+ this.spendSig = sig;
183
+ this.spendCount = (rows ?? []).length;
184
+ this.spendRows.replaceChildren(...(rows ?? []).flatMap(([label, value, note]) => {
185
+ const dd = mk('dd', null, value ?? '');
186
+ dd.title = note ?? '';
187
+ const dt = mk('dt', null, label);
188
+ dt.title = note ?? '';
189
+ return [dt, dd];
190
+ }));
191
+ this.spendEl.hidden = !this.spendOpen || !this.spendCount;
147
192
  }
148
193
 
149
194
  /** What the lines and the two marks mean. Shown until it is closed, and then not again unless asked for. */
@@ -169,6 +214,10 @@ export class Canvas {
169
214
  const rows = mk('div', 'cv-legend-grid');
170
215
  rows.append(line('live', 'working now'), line('waiting', 'needs you'), line('done', 'finished'), line('failed', 'failed'));
171
216
  box.append(rows);
217
+ // The one wire that is not a call: in the Order view, from an agent to the wave called after it reported.
218
+ const inferred = line('inferred', 'after it reported (inferred, not a call)');
219
+ inferred.title = INFERRED_NOTE;
220
+ box.append(inferred);
172
221
  const marks = mk('div', 'cv-legend-grid');
173
222
  this.legendBuiltin = mk('span', 'cv-tile cv-tile-sm');
174
223
  const crew = mk('span', 'cv-tile cv-tile-sm');
@@ -196,7 +245,7 @@ export class Canvas {
196
245
  this.root.addEventListener('pointerdown', (e) => {
197
246
  if (e.button !== 0) return; // right/middle must not pan
198
247
  const t = e.target;
199
- if (t.closest?.('.cv-node') || t.closest?.('.cv-minimap') || t.closest?.('.cv-legend')) return;
248
+ if (t.closest?.('.cv-node') || t.closest?.('.cv-minimap') || t.closest?.('.cv-legend')) return; // the summary box is a .cv-legend too
200
249
  panning = { px: e.clientX, py: e.clientY, x: this.view.x, y: this.view.y };
201
250
  this.root.setPointerCapture(e.pointerId);
202
251
  this.root.classList.add('cv-panning');
@@ -303,7 +352,7 @@ export class Canvas {
303
352
  drawnAs: this.lastPlan?.density ?? null,
304
353
  stacked: Boolean(this.lastPlan?.stacked),
305
354
  drawn: this.drawn,
306
- focus: this.focusId ? (this.nodes.get(this.focusId)?.agentType ?? this.focusId) : null,
355
+ focus: this.focusId ? (roleName(this.nodes.get(this.focusId)?.agentType) || this.focusId) : null,
307
356
  groups: (this.lastPlan?.items ?? []).filter((it) => it.kind === 'group').length,
308
357
  };
309
358
  }
@@ -384,12 +433,14 @@ export class Canvas {
384
433
  }
385
434
 
386
435
  expandAll() {
387
- for (const it of this.lastPlan?.items ?? []) if (it.kind === 'group') this.open.set(it.id, true);
436
+ // Beside itself an agent group opens one at a time, by its own card; "all" is then every other kind of group.
437
+ const mine = (it) => it.kind === 'group' && !(this.aside && it.type === 'type');
438
+ for (const it of this.lastPlan?.items ?? []) if (mine(it)) this.open.set(it.id, true);
388
439
  // Opening one level can reveal groups inside it; open those too.
389
440
  for (let i = 0; i < 4; i += 1) {
390
441
  const p = this.#plan();
391
442
  let more = false;
392
- for (const it of p.items) if (it.kind === 'group' && !it.open) { this.open.set(it.id, true); more = true; }
443
+ for (const it of p.items) if (mine(it) && !it.open) { this.open.set(it.id, true); more = true; }
393
444
  if (!more) break;
394
445
  }
395
446
  this.sessionY = null;
@@ -443,6 +494,7 @@ export class Canvas {
443
494
  #plan() {
444
495
  return plan(this.#visibleNodes(), {
445
496
  group: this.group, density: this.density, seq: this.seq, open: this.open, pinned: this.pinned, sessionY: this.sessionY,
497
+ aside: this.aside,
446
498
  });
447
499
  }
448
500
 
@@ -569,7 +621,7 @@ export class Canvas {
569
621
 
570
622
  #drawItem(it, liveCells) {
571
623
  let el = this.els.get(it.id);
572
- const shape = it.kind === 'group' ? `group:${it.open ? `open:${it.grid ? 'grid' : 'rows'}` : 'folded'}`
624
+ const shape = it.kind === 'group' ? `group:${it.boxed ? `open:${it.grid ? 'grid' : 'rows'}` : 'folded'}`
573
625
  : `${it.kind}:${it.compact ? 'chip' : 'card'}`;
574
626
  // A card that changes what it IS (an agent that becomes a chip, a group that opens) is rebuilt; one that only
575
627
  // changes what it says is updated in place.
@@ -600,7 +652,7 @@ export class Canvas {
600
652
  el.classList.toggle('cv-pinned', Boolean(it.pinned));
601
653
  if (it.kind === 'session') this.#fillSession(el, it);
602
654
  else if (it.kind === 'group') this.#fillGroup(el, it, liveCells);
603
- else this.#fillAgent(el, it.node, it.compact);
655
+ else this.#fillAgent(el, it.node, it.compact, it.after ?? null);
604
656
  }
605
657
 
606
658
  /** The skeleton of a card: the parts it will fill. */
@@ -627,7 +679,7 @@ export class Canvas {
627
679
  p.head.append(p.tile, p.name, mk('span', 'cv-fill'), p.sub, p.fold);
628
680
  p.bar = mk('div', 'cv-bar');
629
681
  p.count = mk('span', 'cv-sub');
630
- if (it.open) {
682
+ if (it.boxed) {
631
683
  el.classList.add('cv-open');
632
684
  el.append(p.head, p.bar);
633
685
  } else {
@@ -639,18 +691,24 @@ export class Canvas {
639
691
  } else if (it.compact) {
640
692
  el.classList.add('cv-chip');
641
693
  p.dot = mk('span', 'dot');
694
+ p.order = mk('span', 'cv-order');
642
695
  p.name = mk('span', 'cv-name');
643
696
  p.task = mk('span', 'cv-task');
644
697
  p.word = mk('span', 'cv-word');
645
- el.append(p.dot, p.name, p.task, p.word);
698
+ el.append(p.dot, p.order, p.name, p.task, p.word);
646
699
  } else {
647
700
  p.tile = mk('span', 'cv-tile');
648
701
  p.name = mk('span', 'cv-name');
702
+ p.order = mk('span', 'cv-order');
649
703
  p.pill = this.#pill();
650
704
  const row = mk('div', 'cv-row');
651
705
  row.append(p.tile, p.name, mk('span', 'cv-fill'), p.pill.el);
652
- p.task = mk('div', 'cv-task cv-indent');
653
- el.append(row, p.task);
706
+ // The second line: the task, and at its end where the card stands in the order of the calls. The name
707
+ // keeps the first line to itself.
708
+ p.task = mk('span', 'cv-task');
709
+ const under = mk('div', 'cv-under cv-indent');
710
+ under.append(p.task, p.order);
711
+ el.append(row, under);
654
712
  }
655
713
  el.parts = p;
656
714
  }
@@ -701,7 +759,8 @@ export class Canvas {
701
759
  #fillSession(el, it) {
702
760
  const n = it.node;
703
761
  const st = this.sessionState;
704
- const sig = JSON.stringify([n.gitBranch, n.cwd, n.turns, n.tokens, st?.word, st?.tone]);
762
+ const bg = n.backgroundNow?.length ?? 0;
763
+ const sig = JSON.stringify([n.gitBranch, n.cwd, n.turns, n.tokens, st?.word, st?.tone, bg]);
705
764
  if (el.sig === sig) return;
706
765
  el.sig = sig;
707
766
  const p = el.parts;
@@ -712,18 +771,23 @@ export class Canvas {
712
771
  p.name.textContent = n.gitBranch || shortPath(n.cwd) || n.sessionId || '';
713
772
  const bits = [`${n.turns ?? 0} ${n.turns === 1 ? 'turn' : 'turns'}`];
714
773
  if (n.tokens != null) bits.push(`${fmtTokens(n.tokens)} ctx`);
774
+ // What the session has running that is not an agent: commands sent to the background.
775
+ if (bg) bits.push(`${bg} in background`);
715
776
  p.sub.textContent = bits.join(' · ');
777
+ p.sub.title = bg ? n.backgroundNow.map((c) => `In the background: ${c.toolName}${c.detail ? ` · ${c.detail}` : ''}`).join('\n') : '';
716
778
  el.title = 'Click to inspect this session';
717
779
  el.setAttribute('aria-label', `Session, ${st?.word ?? 'state not measured'}, ${p.name.textContent}, ${p.sub.textContent}`);
718
780
  }
719
781
 
720
- #fillAgent(el, n, compact) {
782
+ #fillAgent(el, n, compact, after = null) {
721
783
  const st = this.#statusOf(n);
722
784
  const who = compact ? this.#identity(n) : this.#fillTile(el.parts.tile, n);
723
- const type = n.kind === 'workflow' ? (n.workflowId ?? 'workflow run') : (n.agentType ?? 'unknown agent');
785
+ const type = n.kind === 'workflow' ? (n.workflowId ?? 'workflow run') : shownType(n);
724
786
  const task = n.kind === 'workflow' ? `${n.members ?? 0} agents` : (n.description ?? '');
725
787
  const dim = this.dimmed(n);
726
- const sig = JSON.stringify([type, task, st.word, st.tone, st.state, who.source, dim, n.status]);
788
+ // Its place in the order the session called its agents, and when: "#3 · 09:14". A chip has room for the number.
789
+ const tag = orderTag(n, compact ? null : clock);
790
+ const sig = JSON.stringify([type, task, st.word, st.tone, st.state, who.source, dim, n.status, tag, after]);
727
791
  if (el.sig === sig) return;
728
792
  el.sig = sig;
729
793
  const p = el.parts;
@@ -735,6 +799,14 @@ export class Canvas {
735
799
  el.classList.toggle('cv-dim', dim);
736
800
  p.name.textContent = type;
737
801
  p.task.textContent = task;
802
+ // Both are cut to the card's width; hovering says them whole, and the type as it is declared.
803
+ p.name.title = n.kind === 'workflow' ? '' : (n.agentType ?? '');
804
+ p.task.title = task;
805
+ p.order.textContent = tag;
806
+ p.order.hidden = !tag;
807
+ p.order.title = tag ? `Called ${orderTag(n, clock)} by the session` : '';
808
+ // In the Order view: what had reported before this wave was called. A reading of the order of events.
809
+ el.title = after?.length ? `Called after ${after.join(', ')} reported.\n${INFERRED_NOTE}` : '';
738
810
  if (compact) {
739
811
  p.dot.dataset.tone = st.tone ?? 'none';
740
812
  p.word.textContent = st.word;
@@ -752,19 +824,28 @@ export class Canvas {
752
824
  const p = el.parts;
753
825
  const counts = it.counts;
754
826
  const says = Object.entries(counts).map(([s, c]) => `${c} ${agentStatus(s).word}`).join(' · ');
755
- const sig = JSON.stringify([it.label, says, it.open, it.type, it.status]);
827
+ const sig = JSON.stringify([it.label, says, it.open, it.boxed, it.type, it.status, it.first?.order ?? null]);
756
828
  if (el.sig !== sig) {
757
829
  el.sig = sig;
758
830
  el.dataset.state = stateOf(it.status);
759
831
  el.dataset.status = it.status;
760
832
  el.dataset.group = it.type;
761
833
  el.dataset.open = String(Boolean(it.open));
834
+ // Open beside itself: the card stays a card and its agents are the next column.
835
+ el.dataset.aside = String(Boolean(it.open && !it.boxed));
762
836
  p.name.textContent = it.label;
837
+ p.name.title = it.real ?? '';
763
838
  if (it.type === 'run') { p.tile.innerHTML = MARK.run; p.tile.title = 'Workflow run'; }
764
839
  else this.#fillTile(p.tile, it.type === 'type' ? { agentType: it.agentType } : (it.of ?? {}));
765
- p.sub.textContent = it.type === 'type' ? '' : `${it.members.length} ${it.members.length === 1 ? 'agent' : 'agents'}`;
766
- p.fold.innerHTML = it.open ? MARK.down : MARK.chevron;
767
- p.fold.setAttribute('aria-label', `${it.open ? 'Fold' : 'Open'} ${it.label}`);
840
+ // Several agents of one type: the number is a badge beside the role. Any other group says how many it holds.
841
+ const size = it.type === 'type' ? `× ${it.count}` : `${it.members.length} ${it.members.length === 1 ? 'agent' : 'agents'}`;
842
+ // The group stands where its first member does in the order of the calls.
843
+ p.sub.textContent = [orderTag(it.first), size].filter(Boolean).join(' · ');
844
+ p.sub.classList.toggle('cv-times', it.type === 'type');
845
+ p.fold.innerHTML = it.boxed ? MARK.down : MARK.chevron;
846
+ // Read aloud, the badge is part of the name.
847
+ const said = it.type === 'type' ? `${it.label} × ${it.count}` : it.label;
848
+ p.fold.setAttribute('aria-label', `${it.open ? 'Fold' : 'Open'} ${said}`);
768
849
  p.fold.setAttribute('aria-expanded', String(Boolean(it.open)));
769
850
  // The bar: how the group went, in the order the eye wants it.
770
851
  const order = ['done', 'running', 'starting', 'failed', 'killed', 'stopped', 'stale', 'ended'];
@@ -776,11 +857,11 @@ export class Canvas {
776
857
  return seg;
777
858
  }));
778
859
  p.bar.title = says;
779
- if (!it.open) p.count.textContent = says;
780
- el.title = it.open ? '' : `Click to show ${it.members.length} agents`;
781
- el.setAttribute('aria-label', `${it.label}, ${says}, ${it.open ? 'open' : 'folded'}`);
860
+ if (!it.boxed) p.count.textContent = says;
861
+ el.title = it.boxed ? '' : (it.open ? 'Click to fold' : `Click to show ${it.members.length} agents`);
862
+ el.setAttribute('aria-label', `${said}, ${says}, ${it.open ? 'open' : 'folded'}`);
782
863
  }
783
- if (!it.open) return;
864
+ if (!it.boxed) return;
784
865
 
785
866
  for (const cell of it.cells) {
786
867
  liveCells.add(cell.id);
@@ -795,8 +876,9 @@ export class Canvas {
795
876
  c.tabIndex = 0;
796
877
  const q = {};
797
878
  if (!it.grid) { q.tile = mk('span', 'cv-tile'); c.append(q.tile); } else { q.dot = mk('span', 'dot'); c.append(q.dot); }
879
+ q.order = mk('span', 'cv-order');
798
880
  q.name = mk('span', 'cv-name');
799
- c.append(q.name);
881
+ c.append(q.order, q.name);
800
882
  if (!it.grid) { c.append(mk('span', 'cv-fill')); q.dot = mk('span', 'dot'); c.append(q.dot); }
801
883
  q.word = mk('span', 'cv-word');
802
884
  c.append(q.word);
@@ -816,9 +898,10 @@ export class Canvas {
816
898
  const st = this.#statusOf(n);
817
899
  const who = it.grid ? this.#identity(n) : this.#fillTile(c.parts.tile, n);
818
900
  // Inside a group of one type the type is the group's name, so a member is told apart by its task.
819
- const text = it.type === 'type' ? (n.description || n.agentType || n.id) : (n.agentType ?? 'unknown agent');
901
+ const text = it.type === 'type' ? (n.description || shownType(n) || n.id) : shownType(n);
820
902
  const dim = this.dimmed(n);
821
- const csig = JSON.stringify([text, st.word, st.tone, st.state, dim, this.selected === n.id]);
903
+ const ctag = orderTag(n, it.grid ? null : clock);
904
+ const csig = JSON.stringify([text, st.word, st.tone, st.state, dim, this.selected === n.id, ctag]);
822
905
  if (c.sig === csig) continue;
823
906
  c.sig = csig;
824
907
  c.dataset.state = st.state;
@@ -826,12 +909,39 @@ export class Canvas {
826
909
  c.classList.toggle('cv-dim', dim);
827
910
  c.classList.toggle('cv-selected', this.selected === n.id);
828
911
  c.parts.name.textContent = text;
912
+ c.parts.order.textContent = ctag;
913
+ c.parts.order.hidden = !ctag;
914
+ c.title = hoverOf(n);
829
915
  c.parts.dot.dataset.tone = st.tone ?? 'none';
830
916
  c.parts.word.textContent = st.word;
831
- c.setAttribute('aria-label', `${n.agentType ?? 'unknown agent'}, ${st.word}${n.description ? `, ${n.description}` : ''} (${who.title})`);
917
+ c.setAttribute('aria-label', `${shownType(n)}, ${st.word}${n.description ? `, ${n.description}` : ''} (${who.title})`);
832
918
  }
833
919
  }
834
920
 
921
+ /** What an inferred wire says: on hover that it is an inference, and once per reporter, in words beside it. */
922
+ #edgeWords(path, e, c) {
923
+ const title = e.title ?? '';
924
+ if (path.said !== title) {
925
+ path.said = title;
926
+ path.querySelector?.('title')?.remove();
927
+ if (title) {
928
+ const t = document.createElementNS(SVG, 'title');
929
+ t.textContent = title;
930
+ path.append(t);
931
+ }
932
+ }
933
+ if (!e.label) { path.label?.remove(); path.label = null; return; }
934
+ if (!path.label) {
935
+ path.label = document.createElementNS(SVG, 'text');
936
+ path.label.setAttribute('class', 'cv-edge-label');
937
+ this.edgeG.append(path.label);
938
+ }
939
+ path.label.textContent = e.label;
940
+ // Just off the reporter's edge, above the wire, where it does not sit on a card.
941
+ path.label.setAttribute('x', String(e.from.x + 8));
942
+ path.label.setAttribute('y', String(e.from.y - 6));
943
+ }
944
+
835
945
  // Wires are kept and updated, never rebuilt.
836
946
  //
837
947
  // Replacing the whole layer each pass was the reason motion could not live here: a CSS animation restarts when
@@ -870,9 +980,13 @@ export class Canvas {
870
980
 
871
981
  const target = this.nodes.get(e.target);
872
982
  const waiting = target && this.waiting.has(target.id);
873
- const state = waiting ? 'waiting' : e.state;
983
+ // A wire that is a reading of the order of events says nothing about what its target is doing now.
984
+ const inferred = e.kind === 'inferred';
985
+ const state = inferred ? 'inferred' : (waiting ? 'waiting' : e.state);
874
986
  path.dataset.status = e.status;
875
987
  path.dataset.state = state;
988
+ path.dataset.kind = inferred ? 'inferred' : 'call';
989
+ this.#edgeWords(path, e, c);
876
990
  path.classList.toggle('cv-dim', Boolean(target) && this.dimmed(target));
877
991
  path.classList.toggle('cv-path', onPath.has(e.target));
878
992
 
@@ -888,6 +1002,7 @@ export class Canvas {
888
1002
 
889
1003
  for (const [key, path] of this.edgeEls) {
890
1004
  if (alive.has(key)) continue;
1005
+ path.label?.remove();
891
1006
  path.remove();
892
1007
  this.edgeEls.delete(key);
893
1008
  }
@@ -986,11 +1101,23 @@ export class Canvas {
986
1101
  const it = this.lastPlan?.items.find((x) => x.id === groupId);
987
1102
  if (!it || it.kind !== 'group') return;
988
1103
  this.open.set(groupId, !it.open);
1104
+ // Beside itself, one agent's tasks at a time: a second column of them from two groups reads as one list.
1105
+ if (!it.open && it.type === 'type' && this.aside) {
1106
+ for (const other of this.lastPlan.items) if (other.kind === 'group' && other.type === 'type' && other.id !== groupId) this.open.set(other.id, false);
1107
+ }
989
1108
  this.sessionY = null;
990
1109
  this.#persist();
991
1110
  this.#redraw();
992
1111
  }
993
1112
 
1113
+ /** Whether an agent group opens into the next column (a window with room) or downward (a narrow one). */
1114
+ setAside(on) {
1115
+ if (this.aside === Boolean(on)) return;
1116
+ this.aside = Boolean(on);
1117
+ this.sessionY = null;
1118
+ if (this.lastPlan) this.#redraw({ fit: true });
1119
+ }
1120
+
994
1121
  /** Drop the selection without pretending a card was clicked. */
995
1122
  clearSelection() {
996
1123
  this.selected = null;
@@ -1141,7 +1268,8 @@ export class Canvas {
1141
1268
  if (n.tokens != null) bits.push(`${fmtTokens(n.tokens)} tokens`);
1142
1269
  if (n.errors) bits.push(`${n.errors} ${n.errors === 1 ? 'error' : 'errors'}`);
1143
1270
  this.tipEl.replaceChildren(
1144
- mk('strong', null, n.agentType ?? 'unknown agent'),
1271
+ mk('strong', null, shownType(n)),
1272
+ mk('div', 'cv-sub cv-real', n.agentType ?? ''),
1145
1273
  mk('div', null, n.description || 'No task recorded'),
1146
1274
  mk('div', 'cv-sub', bits.length ? bits.join(' · ') : 'No numbers yet'),
1147
1275
  );
@@ -375,6 +375,7 @@ class Pane {
375
375
  node.sig = sig;
376
376
  const text = el('span', 'deleg-text');
377
377
  text.append(el('span', 'deleg-type', card.type), el('span', 'deleg-task', card.task));
378
+ text.title = [card.real, card.task].filter(Boolean).join('\n');
378
379
  const parts = [];
379
380
  const tile = card.agentId ? this.hooks.tileOf?.(this.id, card.agentId) : null;
380
381
  if (tile) parts.push(tile);
@@ -3,6 +3,7 @@
3
3
  // The same shapes come from two places: the server's reading of a transcript (history, and sessions Studio only
4
4
  // watches) and the live stream of a session started here. Both go through these functions, so a tool row reads
5
5
  // the same whichever way it arrived.
6
+ import { roleName } from './names.js';
6
7
 
7
8
  // How much of a tool's output the page keeps for a live result: the same tail the server keeps for history.
8
9
  const RESULT_TAIL = 4096;
@@ -76,7 +77,8 @@ export function outputOf(block) {
76
77
  export function delegationCard(block, nodes) {
77
78
  const node = block.id ? (nodes ?? []).find((n) => n.kind === 'agent' && n.toolUseId === block.id) ?? null : null;
78
79
  return {
79
- type: block.subagentType ?? node?.agentType ?? 'agent',
80
+ type: roleName(block.subagentType ?? node?.agentType ?? 'agent'),
81
+ real: block.subagentType ?? node?.agentType ?? null,
80
82
  task: block.label ?? node?.description ?? '',
81
83
  agentId: node?.id ?? null,
82
84
  status: node?.status ?? null,
@@ -121,7 +123,7 @@ export function refusals(denials, wasAllowed, resultOf = () => null) {
121
123
  export function reminderOf(waiting) {
122
124
  const list = waiting ?? [];
123
125
  if (!list.length) return null;
124
- const who = list[0].agentType ?? 'This session';
126
+ const who = list[0].agentType ? roleName(list[0].agentType) : 'This session';
125
127
  const more = list.length - 1;
126
128
  return `${who} is waiting for approval${more > 0 ? `, and ${more} more ${more === 1 ? 'request is' : 'requests are'}` : ''}`;
127
129
  }
@@ -184,6 +184,7 @@ export class Dock {
184
184
  p.word.textContent = 'Needs you';
185
185
 
186
186
  p.who.textContent = asker(item);
187
+ p.who.title = item.agentType ? `${item.agentType} — show on the canvas` : 'Show on the canvas';
187
188
  p.tool.textContent = item.toolName;
188
189
  const where = this.where(item);
189
190
  p.where.hidden = !where;