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.
- package/CHANGELOG.md +70 -4
- package/README.md +1 -1
- package/kit/agents/crew-commit-agent.md +5 -2
- package/kit/eval/doctor.sh +26 -0
- package/kit/eval/smoke-test.sh +274 -18
- package/kit/hooks/context-usage.sh +1 -1
- package/kit/hooks/guard-bash.sh +93 -6
- package/kit/hooks/guard-schedule.sh +92 -0
- package/kit/hooks/prompt-approval.sh +68 -21
- package/kit/settings.json +11 -0
- package/kit/skills/crew-approve/SKILL.md +26 -0
- package/kit/studio/server/index.js +18 -0
- package/kit/studio/server/lib/graph.js +137 -15
- package/kit/studio/server/lib/permissions.js +11 -3
- package/kit/studio/server/lib/pricing.js +63 -0
- package/kit/studio/server/lib/usage.js +144 -0
- package/kit/studio/web/app.js +159 -38
- package/kit/studio/web/approvals.js +20 -1
- package/kit/studio/web/canvas.js +158 -30
- package/kit/studio/web/chat.js +1 -0
- package/kit/studio/web/convo.js +4 -2
- package/kit/studio/web/dock.js +1 -0
- package/kit/studio/web/graph-plan.js +173 -13
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +14 -1
- package/kit/studio/web/list-plan.js +4 -4
- package/kit/studio/web/list.js +2 -0
- package/kit/studio/web/names.js +44 -0
- package/kit/studio/web/style.css +64 -10
- package/kit/studio/web/timeline-plan.js +13 -5
- package/kit/studio/web/timeline.js +10 -4
- package/kit/studio/web/usage.js +77 -0
- package/package.json +1 -1
package/kit/studio/web/canvas.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
653
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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.
|
|
780
|
-
el.title = it.open ? '' : `Click to show ${it.members.length} agents
|
|
781
|
-
el.setAttribute('aria-label', `${
|
|
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.
|
|
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
|
|
901
|
+
const text = it.type === 'type' ? (n.description || shownType(n) || n.id) : shownType(n);
|
|
820
902
|
const dim = this.dimmed(n);
|
|
821
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
);
|
package/kit/studio/web/chat.js
CHANGED
|
@@ -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);
|
package/kit/studio/web/convo.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/kit/studio/web/dock.js
CHANGED
|
@@ -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;
|