crewforth 3.1.0-rc.4 → 3.1.0-rc.6
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 +59 -0
- package/README.md +1 -1
- package/kit/AGENT_TEMPLATE.md +48 -19
- package/kit/CLAUDE.md +13 -0
- package/kit/agents/crew-backend-expert.md +8 -0
- package/kit/agents/crew-commit-agent.md +8 -0
- package/kit/agents/crew-database-expert.md +8 -0
- package/kit/agents/crew-devops-expert.md +8 -0
- package/kit/agents/crew-frontend-expert.md +8 -0
- package/kit/agents/crew-performance-expert.md +8 -0
- package/kit/agents/crew-planner.md +14 -0
- package/kit/agents/crew-privacy-agent.md +10 -2
- package/kit/agents/crew-review-agent.md +8 -0
- package/kit/agents/crew-security-expert.md +11 -4
- package/kit/agents/crew-session-manager.md +10 -3
- package/kit/agents/crew-test-expert.md +8 -0
- package/kit/eval/doctor.sh +16 -0
- package/kit/eval/smoke-test.sh +898 -11
- package/kit/hooks/agent-outcome.sh +416 -0
- package/kit/hooks/guard-agent-model.sh +819 -0
- package/kit/hooks/guard-bash.sh +26 -1
- package/kit/hooks/guard-schedule.sh +7 -2
- package/kit/hooks/guard-write.sh +31 -0
- package/kit/hooks/prompt-approval.sh +55 -5
- package/kit/settings.json +54 -0
- package/kit/skills/crew-loosen/SKILL.md +28 -0
- package/kit/studio/server/hooks/studio-gate.sh +90 -4
- package/kit/studio/server/hooks/studio-host.mjs +92 -0
- package/kit/studio/server/index.js +86 -7
- package/kit/studio/server/lib/agent-origin.js +44 -0
- package/kit/studio/server/lib/graph.js +142 -10
- package/kit/studio/server/lib/model-outcomes.js +287 -0
- package/kit/studio/server/lib/palette.js +5 -18
- package/kit/studio/server/lib/permissions.js +260 -17
- package/kit/studio/server/lib/session.js +41 -1
- package/kit/studio/server/lib/usage.js +103 -25
- package/kit/studio/web/app.js +251 -37
- package/kit/studio/web/approvals.js +53 -2
- package/kit/studio/web/ask.js +137 -0
- package/kit/studio/web/canvas.js +220 -59
- package/kit/studio/web/chat.js +19 -2
- package/kit/studio/web/convo.js +14 -1
- package/kit/studio/web/dock.js +30 -9
- package/kit/studio/web/graph-plan.js +66 -18
- package/kit/studio/web/icons/builtin.svg +1 -1
- package/kit/studio/web/index.html +11 -4
- package/kit/studio/web/inspect.js +33 -1
- package/kit/studio/web/keys.js +29 -0
- package/kit/studio/web/list-plan.js +8 -3
- package/kit/studio/web/list.js +35 -7
- package/kit/studio/web/models-plan.js +167 -0
- package/kit/studio/web/models.js +127 -0
- package/kit/studio/web/names.js +84 -0
- package/kit/studio/web/style.css +134 -15
- package/kit/studio/web/timeline-plan.js +8 -0
- package/kit/studio/web/timeline.js +36 -10
- package/kit/studio/web/usage.js +53 -12
- package/package.json +1 -1
package/kit/studio/web/chat.js
CHANGED
|
@@ -370,7 +370,7 @@ class Pane {
|
|
|
370
370
|
|
|
371
371
|
paintCard({ block, node }) {
|
|
372
372
|
const card = delegationCard(block, this.hooks.agentsOf?.(this.id));
|
|
373
|
-
const sig = `${card.type}|${card.task}|${card.agentId}|${card.status}`;
|
|
373
|
+
const sig = `${card.type}|${card.task}|${card.agentId}|${card.status}|${card.model.ran}|${card.model.asked}`;
|
|
374
374
|
if (node.sig === sig) return;
|
|
375
375
|
node.sig = sig;
|
|
376
376
|
const text = el('span', 'deleg-text');
|
|
@@ -380,6 +380,13 @@ class Pane {
|
|
|
380
380
|
const tile = card.agentId ? this.hooks.tileOf?.(this.id, card.agentId) : null;
|
|
381
381
|
if (tile) parts.push(tile);
|
|
382
382
|
parts.push(text);
|
|
383
|
+
// The model: what it ran on, or what the call asks for while the agent has not been seen yet.
|
|
384
|
+
const mword = card.model.ran || card.model.asked;
|
|
385
|
+
if (mword) {
|
|
386
|
+
const badge = el('span', 'deleg-model', `${mword}${card.model.differs ? ' \u2260' : ''}`);
|
|
387
|
+
badge.title = card.model.title;
|
|
388
|
+
parts.push(badge);
|
|
389
|
+
}
|
|
383
390
|
// The agent's state is the graph's. Before the graph has seen the agent there is none to show.
|
|
384
391
|
const st = card.agentId ? this.hooks.statusOf?.(this.id, card.agentId) : null;
|
|
385
392
|
if (st?.word) {
|
|
@@ -438,7 +445,17 @@ class Pane {
|
|
|
438
445
|
d.dataset.tone = h.tone;
|
|
439
446
|
badge.append(d, el('span', null, h.badge));
|
|
440
447
|
const parts = [badge];
|
|
441
|
-
for (const p of h.parts)
|
|
448
|
+
for (const p of h.parts) {
|
|
449
|
+
// The mode is a control: a click offers the modes a session can be in. Everything else in the strip is said.
|
|
450
|
+
if (h.mode && p === `mode: ${h.mode}` && this.hooks.onMode && s?.state !== 'exited' && s?.state !== 'failed') {
|
|
451
|
+
const b = el('button', 'sub pane-mode', p);
|
|
452
|
+
b.type = 'button';
|
|
453
|
+
b.title = 'Change the mode this session is in';
|
|
454
|
+
b.setAttribute('aria-haspopup', 'menu');
|
|
455
|
+
b.addEventListener('click', (e) => { e.stopPropagation(); this.hooks.onMode(this, b, h.mode); });
|
|
456
|
+
parts.push(b);
|
|
457
|
+
} else parts.push(el('span', 'sub', p));
|
|
458
|
+
}
|
|
442
459
|
if (h.ungated) {
|
|
443
460
|
const warn = el('span', 'pill pane-ungated', 'No approval gate');
|
|
444
461
|
warn.title = 'This session was started without the approval gate: its tool calls do not wait for you.';
|
package/kit/studio/web/convo.js
CHANGED
|
@@ -3,7 +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
|
+
import { roleName, modelOf } from './names.js';
|
|
7
7
|
|
|
8
8
|
// How much of a tool's output the page keeps for a live result: the same tail the server keeps for history.
|
|
9
9
|
const RESULT_TAIL = 4096;
|
|
@@ -20,6 +20,8 @@ export function toolBlock(c) {
|
|
|
20
20
|
label: i.description ?? i.file_path ?? i.command ?? i.pattern ?? i.query ?? null,
|
|
21
21
|
id: typeof c?.id === 'string' ? c.id : null,
|
|
22
22
|
subagentType: typeof i.subagent_type === 'string' ? i.subagent_type : null,
|
|
23
|
+
// The model a delegation asks for; null when the call names none.
|
|
24
|
+
model: typeof i.model === 'string' ? i.model : null,
|
|
23
25
|
result: null,
|
|
24
26
|
};
|
|
25
27
|
}
|
|
@@ -82,6 +84,8 @@ export function delegationCard(block, nodes) {
|
|
|
82
84
|
task: block.label ?? node?.description ?? '',
|
|
83
85
|
agentId: node?.id ?? null,
|
|
84
86
|
status: node?.status ?? null,
|
|
87
|
+
// What it ran on once the graph knows the agent; before that, what the call asked for.
|
|
88
|
+
model: modelOf({ model: node?.model ?? null, modelAsked: block.model ?? node?.modelAsked ?? null }),
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -128,6 +132,13 @@ export function reminderOf(waiting) {
|
|
|
128
132
|
return `${who} is waiting for approval${more > 0 ? `, and ${more} more ${more === 1 ? 'request is' : 'requests are'}` : ''}`;
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
// The modes a session started here can be put in: the three it can be started in, and no other.
|
|
136
|
+
export const SESSION_MODES = [
|
|
137
|
+
{ mode: 'plan', label: 'plan', title: 'Explore and plan; nothing is edited until a plan is approved.' },
|
|
138
|
+
{ mode: 'acceptEdits', label: 'acceptEdits', title: 'File edits in the project are accepted; every call is still shown in the dock first.' },
|
|
139
|
+
{ mode: 'default', label: 'default', title: 'Each call is asked about in the dock.' },
|
|
140
|
+
];
|
|
141
|
+
|
|
131
142
|
/** The strip above a conversation: what kind of session this is, in the order the design has it. */
|
|
132
143
|
export function headerOf(session, { readOnly = false, contextTokens = null } = {}) {
|
|
133
144
|
if (readOnly) return { badge: 'Read only', tone: 'none', parts: [] };
|
|
@@ -141,6 +152,8 @@ export function headerOf(session, { readOnly = false, contextTokens = null } = {
|
|
|
141
152
|
badge: 'Started here',
|
|
142
153
|
tone: 'good',
|
|
143
154
|
parts,
|
|
155
|
+
// The mode on its own as well: in the strip it is a control, and the rest of `parts` is not.
|
|
156
|
+
mode: session?.permissionMode ?? null,
|
|
144
157
|
// A session started here without the gate is running unguarded, and that is said in the strip itself.
|
|
145
158
|
ungated: session ? session.gated === false : false,
|
|
146
159
|
};
|
package/kit/studio/web/dock.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// request is not forgotten either, because its clock is on screen and counting. What it says and when comes from
|
|
5
5
|
// approvals.js; this file draws it and hands the three answers back.
|
|
6
6
|
import { remaining, asker, allowSessionLabel, VERDICTS, OUTCOME_WORD } from './approvals.js';
|
|
7
|
+
import { AskForms } from './ask.js';
|
|
7
8
|
|
|
8
9
|
const SVG = 'http://www.w3.org/2000/svg';
|
|
9
10
|
const RING = 2 * Math.PI * 15; // the countdown ring's circumference, r = 15
|
|
@@ -39,6 +40,8 @@ export class Dock {
|
|
|
39
40
|
this.flash = null; // { word, outcome, until }
|
|
40
41
|
this.flashTimer = null;
|
|
41
42
|
this.suppressed = false; // another surface is showing the requests with their answers
|
|
43
|
+
// A question and a plan are answered with a form, not with the three buttons.
|
|
44
|
+
this.forms = new AskForms((item, verdict, extra) => this.decide(verdict, extra, item));
|
|
42
45
|
this.#build();
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -111,7 +114,11 @@ export class Dock {
|
|
|
111
114
|
this.decide(verdict);
|
|
112
115
|
});
|
|
113
116
|
|
|
114
|
-
|
|
117
|
+
// Where the form of a question or a plan goes: under the line that says who asks, in place of the command.
|
|
118
|
+
const ask = mk('div', 'dock-ask');
|
|
119
|
+
ask.hidden = true;
|
|
120
|
+
text.append(ask);
|
|
121
|
+
this.parts = { timer, ring, secs, word, who, tool, where, cmd, note, nav, prev, next, count, acts, deny, always, allow, ask };
|
|
115
122
|
this.root.replaceChildren(timer, text, nav, acts);
|
|
116
123
|
}
|
|
117
124
|
|
|
@@ -125,6 +132,7 @@ export class Dock {
|
|
|
125
132
|
this.at = keep === -1 ? Math.min(this.at, Math.max(0, this.items.length - 1)) : keep;
|
|
126
133
|
if (keep === -1) this.open = false;
|
|
127
134
|
for (const key of [...this.busy]) if (!this.items.some((r) => r.key === key)) this.busy.delete(key);
|
|
135
|
+
this.forms.keep(this.items.map((r) => r.key));
|
|
128
136
|
this.paint();
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -135,18 +143,21 @@ export class Dock {
|
|
|
135
143
|
this.paint();
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
decide(verdict) {
|
|
139
|
-
const item = this.current;
|
|
146
|
+
decide(verdict, extra = {}, of = null) {
|
|
147
|
+
const item = of ?? this.current;
|
|
140
148
|
// While the dock is saying what became of the last request, the next one is not on screen yet, and a key
|
|
141
149
|
// pressed then must not answer something nobody has read.
|
|
142
150
|
if (!item || this.busy.has(item.key) || this.flashing) return;
|
|
151
|
+
// The keys a/s/d answer a command. A question and a plan are answered in their form, or denied.
|
|
152
|
+
if (item.ask && !['answer', 'plan', 'deny'].includes(verdict)) return;
|
|
143
153
|
this.busy.add(item.key);
|
|
154
|
+
this.forms.setSent(item.key, true);
|
|
144
155
|
this.paint();
|
|
145
|
-
this.onDecide(item, verdict);
|
|
156
|
+
this.onDecide(item, verdict, extra);
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
/** An answer could not be delivered: the buttons come back. */
|
|
149
|
-
release(key) { this.busy.delete(key); this.paint(); }
|
|
160
|
+
release(key) { this.busy.delete(key); this.forms.setSent(key, false); this.paint(); }
|
|
150
161
|
|
|
151
162
|
/** Say what became of a request for a moment: "Denied", "Timed out — denied". */
|
|
152
163
|
say(outcome, queued = false) {
|
|
@@ -173,7 +184,7 @@ export class Dock {
|
|
|
173
184
|
if (this.root.hidden) return;
|
|
174
185
|
|
|
175
186
|
// What became of the last request is said in the place it was, before the next one takes it.
|
|
176
|
-
for (const part of [p.timer, p.who, p.tool, p.where, p.cmd, p.note, p.nav, p.acts]) part.hidden = flashing;
|
|
187
|
+
for (const part of [p.timer, p.who, p.tool, p.where, p.cmd, p.note, p.nav, p.acts, p.ask]) part.hidden = flashing;
|
|
177
188
|
if (flashing) {
|
|
178
189
|
this.root.dataset.state = this.flash.outcome;
|
|
179
190
|
p.word.textContent = this.flash.word;
|
|
@@ -189,7 +200,15 @@ export class Dock {
|
|
|
189
200
|
const where = this.where(item);
|
|
190
201
|
p.where.hidden = !where;
|
|
191
202
|
p.where.textContent = where ? `in ${where}` : '';
|
|
192
|
-
|
|
203
|
+
// A question or a plan: its form, in place of the command and the three buttons.
|
|
204
|
+
const form = this.forms.get(item);
|
|
205
|
+
p.ask.hidden = !form;
|
|
206
|
+
p.acts.hidden = Boolean(form);
|
|
207
|
+
// Put in once and left there: a form taken out and put back on every tick would lose the caret in it.
|
|
208
|
+
if (this.shownForm !== form) { p.ask.replaceChildren(...(form ? [form] : [])); this.shownForm = form; }
|
|
209
|
+
this.root.dataset.ask = form ? item.ask.kind : '';
|
|
210
|
+
p.tool.textContent = form ? (item.ask.kind === 'plan' ? 'wants its plan approved' : 'asks') : item.toolName;
|
|
211
|
+
p.cmd.hidden = !item.detail || Boolean(form);
|
|
193
212
|
p.cmd.textContent = item.detail ?? '';
|
|
194
213
|
p.cmd.classList.toggle('open', this.open);
|
|
195
214
|
p.cmd.setAttribute('aria-expanded', String(this.open));
|
|
@@ -200,7 +219,7 @@ export class Dock {
|
|
|
200
219
|
p.ring.style.strokeDasharray = `${(r.fraction * RING).toFixed(1)} ${RING.toFixed(1)}`;
|
|
201
220
|
p.secs.textContent = `${r.left}s`;
|
|
202
221
|
p.timer.setAttribute('aria-label', `Auto-deny in ${r.left} seconds`);
|
|
203
|
-
p.note.textContent = `Auto-deny in ${r.left}s if nobody answers`;
|
|
222
|
+
p.note.textContent = form ? `Refused in ${r.left}s if nobody answers` : `Auto-deny in ${r.left}s if nobody answers`;
|
|
204
223
|
} else {
|
|
205
224
|
// The server did not say how long the hook waits. No clock is better than an invented one.
|
|
206
225
|
p.ring.style.strokeDasharray = `0 ${RING.toFixed(1)}`;
|
|
@@ -217,6 +236,8 @@ export class Dock {
|
|
|
217
236
|
p.always.textContent = allowSessionLabel(item.toolName);
|
|
218
237
|
p.always.title = `Allow this call, and stop asking here about ${item.toolName} for the rest of the session. Later calls are not approved for Claude Code: it still applies its own checks to them.`;
|
|
219
238
|
for (const b of [p.deny, p.always, p.allow]) b.disabled = sent;
|
|
220
|
-
this.root.setAttribute('aria-label',
|
|
239
|
+
this.root.setAttribute('aria-label', form
|
|
240
|
+
? `Waiting for you: ${asker(item)} ${item.ask.kind === 'plan' ? 'wants its plan approved' : 'asks a question'}`
|
|
241
|
+
: `Waiting for approval: ${asker(item)} wants to run ${item.toolName}`);
|
|
221
242
|
}
|
|
222
243
|
}
|
|
@@ -24,8 +24,10 @@ export const SIZE = {
|
|
|
24
24
|
colGap: 56, // between one depth and the next
|
|
25
25
|
waveGap: 148, // between one wave and the next: room for the words on an inferred wire
|
|
26
26
|
rowGap: 12, // between siblings
|
|
27
|
-
session: { w: 200, h: 84 },
|
|
28
|
-
card: { w: 248, h:
|
|
27
|
+
session: { w: 200, h: 84, line: 20 }, // `line`: the extra line a session with background commands has
|
|
28
|
+
card: { w: 248, h: 84 }, // an agent: its name, its task, and a line of facts (model, time, tokens)
|
|
29
|
+
stack: { w: 248, h: 116, tight: 64 }, // a group drawn as one card: its name, its bar, its totals, its models;
|
|
30
|
+
// `tight` at compact density, where the two lines of totals go
|
|
29
31
|
chip: { w: 248, h: 36 }, // an agent at compact density
|
|
30
32
|
group: { // an open group: a container holding its members
|
|
31
33
|
padX: 8, top: 10, head: 20, gap: 6, bar: 4, bottom: 8,
|
|
@@ -45,6 +47,9 @@ export const AUTO = {
|
|
|
45
47
|
// fitting leaves 676 after its margin; a card's 12px name reads down to 9px, which is a zoom of 0.75.
|
|
46
48
|
// 676 / 0.75 = 901.
|
|
47
49
|
tallest: 900,
|
|
50
|
+
// The smallest zoom a card is still read at: its 12px name is 9px there. Auto asks one thing of a picture —
|
|
51
|
+
// does it fit the pane at this zoom or larger — and only goes to chips when the answer is no.
|
|
52
|
+
readable: 0.75,
|
|
48
53
|
// Same-type siblings fold into one card from this many, once the picture is too tall.
|
|
49
54
|
stackAt: 3,
|
|
50
55
|
};
|
|
@@ -153,7 +158,10 @@ function itemsOf(nodes, { group, density, seq, stack, aside }) {
|
|
|
153
158
|
const session = nodes.find((n) => n.kind === 'session');
|
|
154
159
|
if (session) items.push({ id: session.id, kind: 'session', node: session, parent: null, depth: 0 });
|
|
155
160
|
|
|
156
|
-
|
|
161
|
+
// Agents of one type under one parent are one card under `type` and under `run`, from two and wherever they
|
|
162
|
+
// stand in the order: three Privacy agents called fifth, eighth and ninth are the same thing three times.
|
|
163
|
+
// Under `parent` they fold only when the picture is too tall; under `none`, never.
|
|
164
|
+
const stackAt = group === 'type' || group === 'run' ? 2 : (density === 'auto' && group !== 'none' && stack ? AUTO.stackAt : Infinity);
|
|
157
165
|
|
|
158
166
|
// `anchor` is the item a node's wire comes out of; `via` is the member row inside it, when the parent is one.
|
|
159
167
|
const place = (parentId, anchor, via, depth) => {
|
|
@@ -185,7 +193,7 @@ function itemsOf(nodes, { group, density, seq, stack, aside }) {
|
|
|
185
193
|
label: roleName(t), count: members.length, real: t, agentType: members[0].agentType ?? null,
|
|
186
194
|
});
|
|
187
195
|
items.push(g);
|
|
188
|
-
g.aside = Boolean(aside?.(g
|
|
196
|
+
g.aside = Boolean(aside?.(g));
|
|
189
197
|
for (const m of members) {
|
|
190
198
|
stacked.add(m.id);
|
|
191
199
|
if (!g.aside) { place(m.id, g, m.id, depth + 1); continue; }
|
|
@@ -216,6 +224,23 @@ function itemsOf(nodes, { group, density, seq, stack, aside }) {
|
|
|
216
224
|
return items;
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
/**
|
|
228
|
+
* What a group's members add up to: time, new tokens, tokens read from the cache, and the estimate. A member with
|
|
229
|
+
* no usage read adds nothing; the cost is null when any member's is, so a sum never passes for the whole.
|
|
230
|
+
*/
|
|
231
|
+
export function sumOf(members) {
|
|
232
|
+
const sum = { durationMs: 0, fresh: 0, cacheRead: 0, cost: 0, timed: 0, counted: 0 };
|
|
233
|
+
for (const m of members ?? []) {
|
|
234
|
+
if (m.durationMs != null) { sum.durationMs += m.durationMs; sum.timed += 1; }
|
|
235
|
+
if (!m.usage) continue;
|
|
236
|
+
sum.counted += 1;
|
|
237
|
+
sum.fresh += m.usage.tokens?.fresh ?? 0;
|
|
238
|
+
sum.cacheRead += m.usage.tokens?.cacheRead ?? 0;
|
|
239
|
+
sum.cost = sum.cost === null || m.usage.cost == null ? null : sum.cost + m.usage.cost;
|
|
240
|
+
}
|
|
241
|
+
return sum;
|
|
242
|
+
}
|
|
243
|
+
|
|
219
244
|
function groupItem(id, type, members, anchor, via, depth, seq, extra) {
|
|
220
245
|
return {
|
|
221
246
|
id,
|
|
@@ -230,10 +255,24 @@ function groupItem(id, type, members, anchor, via, depth, seq, extra) {
|
|
|
230
255
|
seq: Math.min(...members.map((m) => seq.get(m.id) ?? 0), Infinity),
|
|
231
256
|
// The group stands where its first member does in the order the session called its agents.
|
|
232
257
|
first: members.filter((m) => m.order != null).sort((a, b) => a.order - b.order)[0] ?? null,
|
|
258
|
+
sum: sumOf(members),
|
|
233
259
|
...extra,
|
|
234
260
|
};
|
|
235
261
|
}
|
|
236
262
|
|
|
263
|
+
/**
|
|
264
|
+
* How tall a picture may be before Auto gives up cards for chips: what fits `paneHeight` at the smallest readable
|
|
265
|
+
* zoom, and never less than the reference window's. The count of agents is not asked; a tall window keeps cards
|
|
266
|
+
* for a session a short one turns to chips.
|
|
267
|
+
*/
|
|
268
|
+
export function tallestFor(paneHeight) {
|
|
269
|
+
const fits = typeof paneHeight === 'number' && paneHeight > 0 ? Math.floor((paneHeight - 2 * ZOOM.pad) / AUTO.readable) : 0;
|
|
270
|
+
return Math.max(AUTO.tallest, fits);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A session's card: one line taller while it has commands running in the background, and never wider. */
|
|
274
|
+
export const sessionHeight = (n) => SIZE.session.h + (n?.backgroundNow?.length ? SIZE.session.line : 0);
|
|
275
|
+
|
|
237
276
|
/* ----------------------------------------------------------------- plan --- */
|
|
238
277
|
|
|
239
278
|
/**
|
|
@@ -242,8 +281,11 @@ function groupItem(id, type, members, anchor, via, depth, seq, extra) {
|
|
|
242
281
|
* @param opts.density one of DENSITIES
|
|
243
282
|
* @param opts.seq Map id -> arrival number, kept by the caller across calls (see `sequence`)
|
|
244
283
|
* @param opts.open Map groupId -> boolean, the viewer's own folds; a group not in it takes its default
|
|
284
|
+
* @param opts.all true after "Expand all", false after "Fold all": what a group not in `open` is
|
|
245
285
|
* @param opts.pinned Map itemId -> {x, y}, positions the viewer set by hand
|
|
246
286
|
* @param opts.sessionY where the session card was put, to keep it there; omitted on a fresh layout
|
|
287
|
+
* @param opts.tallest how tall the picture may be before Auto turns cards into chips (see `tallestFor`);
|
|
288
|
+
* the reference window's when it is not given
|
|
247
289
|
* @param opts.aside true when an open type group puts its agents in the next column instead of growing
|
|
248
290
|
* downward; a narrow window leaves it off
|
|
249
291
|
* @returns { items, edges, width, height, drawn, density, sessionY }
|
|
@@ -251,14 +293,18 @@ function groupItem(id, type, members, anchor, via, depth, seq, extra) {
|
|
|
251
293
|
export function plan(nodes, opts = {}) {
|
|
252
294
|
const group = GROUPINGS.includes(opts.group) ? opts.group : 'run';
|
|
253
295
|
const asked = DENSITIES.includes(opts.density) ? opts.density : 'auto';
|
|
254
|
-
|
|
296
|
+
const tallest = Math.max(AUTO.tallest, opts.tallest ?? 0);
|
|
297
|
+
if (group === 'order') return planOrder(nodes, asked, opts, tallest);
|
|
255
298
|
const seq = opts.seq ?? sequence(nodes);
|
|
256
299
|
const open = opts.open ?? new Map();
|
|
257
300
|
const pinned = opts.pinned ?? new Map();
|
|
258
301
|
|
|
259
302
|
// A type group starts folded: it is the same thing several times. A run or an agent's children start open.
|
|
260
|
-
|
|
261
|
-
|
|
303
|
+
// A group is as the viewer left it; one they have not touched follows the last "Expand all" or "Fold all"
|
|
304
|
+
// (`opts.all`), and before either a type group starts folded — it is the same thing several times — while a
|
|
305
|
+
// run or an agent's children start open.
|
|
306
|
+
const isOpen = (g) => (open.has(g.id) ? open.get(g.id) : (typeof opts.all === 'boolean' ? opts.all : g.type !== 'type'));
|
|
307
|
+
const aside = opts.aside ? (g) => isOpen(g) : null;
|
|
262
308
|
|
|
263
309
|
// What is drawn, and how many agents of it the viewer reads one by one: a card, a row or a chip each.
|
|
264
310
|
const drawFrom = (items) => {
|
|
@@ -284,7 +330,7 @@ export function plan(nodes, opts = {}) {
|
|
|
284
330
|
/** Size and place everything drawn, at one density. Returns the box the picture needs. */
|
|
285
331
|
const lay = (drawnItems, compact) => {
|
|
286
332
|
for (const it of drawnItems) {
|
|
287
|
-
if (it.kind === 'session') { it.w = SIZE.session.w; it.h =
|
|
333
|
+
if (it.kind === 'session') { it.w = SIZE.session.w; it.h = sessionHeight(it.node); continue; }
|
|
288
334
|
if (it.kind !== 'group') {
|
|
289
335
|
const sz = compact ? SIZE.chip : SIZE.card;
|
|
290
336
|
it.w = sz.w; it.h = sz.h; it.compact = compact;
|
|
@@ -293,7 +339,8 @@ export function plan(nodes, opts = {}) {
|
|
|
293
339
|
it.open = isOpen(it);
|
|
294
340
|
// `boxed` is the group drawn as a container with its members inside. Open beside itself it stays a card.
|
|
295
341
|
it.boxed = it.open && !it.aside;
|
|
296
|
-
|
|
342
|
+
it.compact = compact;
|
|
343
|
+
if (!it.boxed) { it.w = SIZE.stack.w; it.h = compact ? SIZE.stack.tight : SIZE.stack.h; it.cells = null; continue; }
|
|
297
344
|
const n = it.members.length;
|
|
298
345
|
const grid = compact || n > G.rowsMax;
|
|
299
346
|
const cols = grid ? Math.min(G.colsMax, Math.max(1, Math.ceil(n / G.rowsMax))) : 1;
|
|
@@ -375,12 +422,12 @@ export function plan(nodes, opts = {}) {
|
|
|
375
422
|
let compact = asked === 'compact';
|
|
376
423
|
let stacked = false;
|
|
377
424
|
let box = lay(pass.shown, compact);
|
|
378
|
-
if (asked === 'auto' && box.height >
|
|
425
|
+
if (asked === 'auto' && box.height > tallest) {
|
|
379
426
|
if (group !== 'none' && group !== 'type') {
|
|
380
427
|
const folded = drawFrom(itemsOf(nodes, { group, density: asked, seq, stack: true, aside }));
|
|
381
428
|
if (folded.shown.some((it) => it.kind === 'group' && it.type === 'type')) { pass = folded; stacked = true; box = lay(pass.shown, false); }
|
|
382
429
|
}
|
|
383
|
-
if (box.height >
|
|
430
|
+
if (box.height > tallest) { compact = true; box = lay(pass.shown, true); }
|
|
384
431
|
}
|
|
385
432
|
const drawnItems = pass.shown;
|
|
386
433
|
const drawn = pass.count;
|
|
@@ -443,9 +490,10 @@ export const INFERRED_NOTE = 'Inferred from the order of events: this wave was c
|
|
|
443
490
|
* each agent that reported after the wave before it was called and before this one was — an inference from the
|
|
444
491
|
* order of events, drawn as one and labelled as one. A wave nothing reported before has no wire.
|
|
445
492
|
*/
|
|
446
|
-
function planOrder(nodes, asked, opts) {
|
|
493
|
+
function planOrder(nodes, asked, opts, tallest = AUTO.tallest) {
|
|
447
494
|
const pinned = opts.pinned ?? new Map();
|
|
448
495
|
const session = nodes.find((n) => n.kind === 'session') ?? null;
|
|
496
|
+
const sessionH = sessionHeight(session);
|
|
449
497
|
const agents = nodes.filter((n) => n.kind === 'agent');
|
|
450
498
|
const called = agents.filter((n) => n.wave != null);
|
|
451
499
|
const waveIds = [...new Set(called.map((n) => n.wave))].sort((a, b) => a - b);
|
|
@@ -475,7 +523,7 @@ function planOrder(nodes, asked, opts) {
|
|
|
475
523
|
};
|
|
476
524
|
let compact = asked === 'compact';
|
|
477
525
|
let box = lay(compact);
|
|
478
|
-
if (asked === 'auto' && box.height >
|
|
526
|
+
if (asked === 'auto' && box.height > tallest) { compact = true; box = lay(true); }
|
|
479
527
|
const { items } = box;
|
|
480
528
|
let { height } = box;
|
|
481
529
|
|
|
@@ -483,11 +531,11 @@ function planOrder(nodes, asked, opts) {
|
|
|
483
531
|
if (session) {
|
|
484
532
|
const first = items.filter((it) => it.depth === 1);
|
|
485
533
|
if (sessionY == null) {
|
|
486
|
-
const bottom = first.length ? Math.max(...first.map((it) => it.y + it.h)) : SIZE.padY +
|
|
487
|
-
sessionY = Math.max(SIZE.padY, Math.round((SIZE.padY + bottom) / 2 -
|
|
534
|
+
const bottom = first.length ? Math.max(...first.map((it) => it.y + it.h)) : SIZE.padY + sessionH;
|
|
535
|
+
sessionY = Math.max(SIZE.padY, Math.round((SIZE.padY + bottom) / 2 - sessionH / 2));
|
|
488
536
|
}
|
|
489
|
-
items.unshift({ id: session.id, kind: 'session', node: session, parent: null, depth: 0, w: SIZE.session.w, h:
|
|
490
|
-
height = Math.max(height, sessionY +
|
|
537
|
+
items.unshift({ id: session.id, kind: 'session', node: session, parent: null, depth: 0, w: SIZE.session.w, h: sessionH, x: SIZE.padX, y: sessionY });
|
|
538
|
+
height = Math.max(height, sessionY + sessionH + SIZE.padY);
|
|
491
539
|
}
|
|
492
540
|
for (const it of items) {
|
|
493
541
|
const p = pinned.get(it.id);
|
|
@@ -538,7 +586,7 @@ export function crowdFolds(nodes, opts = {}) {
|
|
|
538
586
|
const folds = [];
|
|
539
587
|
for (let guard = 0; guard < 64; guard += 1) {
|
|
540
588
|
const p = plan(nodes, { ...opts, open, pinned: new Map(), sessionY: null });
|
|
541
|
-
if (p.height <= AUTO.tallest) break;
|
|
589
|
+
if (p.height <= Math.max(AUTO.tallest, opts.tallest ?? 0)) break;
|
|
542
590
|
const next = p.items.filter((it) => it.kind === 'group' && it.open && it.members.length > 1)
|
|
543
591
|
.sort((a, b) => b.members.length - a.members.length || String(a.id).localeCompare(String(b.id)))[0];
|
|
544
592
|
if (!next) break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="16" height="16" fill="#D97757" role="img" aria-label="Claude Code agent"><path d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"/></svg>
|
|
@@ -92,11 +92,14 @@
|
|
|
92
92
|
<button id="view-list" class="seg-btn" type="button" role="tab" aria-label="List view" aria-selected="false" aria-controls="list">
|
|
93
93
|
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M5 4h9M5 8h9M5 12h9M2 4h.01M2 8h.01M2 12h.01"/></svg><span class="tb-words">List</span>
|
|
94
94
|
</button>
|
|
95
|
+
<button id="view-models" class="seg-btn" type="button" role="tab" aria-label="Models view" aria-selected="false" aria-controls="models">
|
|
96
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 13V8M8 13V3M13 13V6"/></svg><span class="tb-words">Models</span>
|
|
97
|
+
</button>
|
|
95
98
|
</div>
|
|
96
99
|
<button id="tb-group" data-view="graph timeline" class="btn sm ghost tb-menu" type="button" aria-haspopup="menu">Group <span>Workflow run</span><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></button>
|
|
97
100
|
<button id="tb-density" data-view="graph" class="btn sm ghost tb-menu" type="button" aria-haspopup="menu">Density <span>Auto</span><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></button>
|
|
98
|
-
<button id="tb-show" class="btn sm ghost tb-menu" type="button" aria-haspopup="menu">Show <span>All</span><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></button>
|
|
99
|
-
<button id="tb-options" class="btn sm ghost tb-menu" type="button" aria-haspopup="menu" aria-label="View options"><span class="tb-name">View<span class="tb-more"> options</span></span><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></button>
|
|
101
|
+
<button id="tb-show" data-view="graph timeline list" class="btn sm ghost tb-menu" type="button" aria-haspopup="menu">Show <span>All</span><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></button>
|
|
102
|
+
<button id="tb-options" data-view="graph timeline list" class="btn sm ghost tb-menu" type="button" aria-haspopup="menu" aria-label="View options"><span class="tb-name">View<span class="tb-more"> options</span></span><svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg></button>
|
|
100
103
|
<span id="foot-note" class="toolbar-note" role="status"></span>
|
|
101
104
|
<span class="toolbar-fill"></span>
|
|
102
105
|
<span class="tb-follow" data-view="timeline" hidden>
|
|
@@ -110,10 +113,10 @@
|
|
|
110
113
|
<button id="tb-range-in" class="btn icon sm ghost" type="button" data-view="timeline" aria-label="Show less time" hidden>
|
|
111
114
|
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg>
|
|
112
115
|
</button>
|
|
113
|
-
<button id="tb-expand" data-view="graph" class="btn sm" type="button" aria-label="Expand all groups">
|
|
116
|
+
<button id="tb-expand" data-view="graph timeline list" class="btn sm" type="button" aria-label="Expand all groups">
|
|
114
117
|
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6l5 5 5-5"/></svg><span class="tb-words">Expand all</span>
|
|
115
118
|
</button>
|
|
116
|
-
<button id="tb-fold" data-view="graph" class="btn sm" type="button" aria-label="Fold all groups">
|
|
119
|
+
<button id="tb-fold" data-view="graph timeline list" class="btn sm" type="button" aria-label="Fold all groups">
|
|
117
120
|
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg><span class="tb-words">Fold all</span>
|
|
118
121
|
</button>
|
|
119
122
|
<button id="tb-zoom-out" data-view="graph" class="btn icon sm ghost" type="button" aria-label="Zoom out">
|
|
@@ -123,6 +126,9 @@
|
|
|
123
126
|
<button id="tb-zoom-in" data-view="graph" class="btn icon sm ghost" type="button" aria-label="Zoom in">
|
|
124
127
|
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg>
|
|
125
128
|
</button>
|
|
129
|
+
<button id="tb-reset" data-view="graph" class="btn sm" type="button" title="Reset layout (r)" aria-label="Reset layout: put the cards moved by hand back where they belong" disabled>
|
|
130
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8a5 5 0 1 0 1.6-3.7M3 3v3h3"/></svg><span class="tb-words">Reset layout</span>
|
|
131
|
+
</button>
|
|
126
132
|
<button id="tb-fit" data-view="graph" class="btn sm" type="button" aria-label="Fit the graph to the view">
|
|
127
133
|
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 5.5v-3h3M10.5 2.5h3v3M13.5 10.5v3h-3M5.5 13.5h-3v-3"/></svg><span class="tb-words">Fit</span>
|
|
128
134
|
</button>
|
|
@@ -132,6 +138,7 @@
|
|
|
132
138
|
<div id="canvas" class="canvas"></div>
|
|
133
139
|
<div id="timeline" role="tabpanel" aria-labelledby="view-timeline" hidden></div>
|
|
134
140
|
<div id="list" role="tabpanel" aria-labelledby="view-list" hidden></div>
|
|
141
|
+
<div id="models" role="tabpanel" aria-labelledby="view-models" hidden></div>
|
|
135
142
|
<div id="first-run" class="first-run" hidden></div>
|
|
136
143
|
<div id="terminal-wait" class="terminal-wait" role="status" hidden></div>
|
|
137
144
|
<div id="dock" role="region" aria-label="Waiting for approval" hidden></div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Functions of the server's data, with no page in them: the four tiles, what the agent is doing right now, which
|
|
4
4
|
// skills it applied, who delegated to it. The rule they share is the panel's own: a value that was not read is
|
|
5
5
|
// written "Not measured", with why — never as 0 and never as an empty list.
|
|
6
|
-
import { roleName } from './names.js';
|
|
6
|
+
import { roleName, modelName, modelFamily, modelOf } from './names.js';
|
|
7
7
|
|
|
8
8
|
const NOT_MEASURED = 'Not measured';
|
|
9
9
|
|
|
@@ -100,6 +100,38 @@ export function backgroundLines(session, now) {
|
|
|
100
100
|
}));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* What the inspector says about an agent's model, as lines: { label, text, goto }.
|
|
105
|
+
*
|
|
106
|
+
* Two facts are kept apart: what the call ASKED for (its own `model` field; none means the agent's default) and
|
|
107
|
+
* what the agent RAN on (the model its own transcript names). Then, when the transcript shows them: the call the
|
|
108
|
+
* model gate refused first, and the same work repeated one model up after a report that closed `confidence: low`.
|
|
109
|
+
* `goto` is the id of the other agent a line is about, for a line that leads somewhere.
|
|
110
|
+
*/
|
|
111
|
+
export function modelLines(n, nodes) {
|
|
112
|
+
if (n?.kind !== 'agent') return [];
|
|
113
|
+
const m = modelOf(n);
|
|
114
|
+
const lines = [
|
|
115
|
+
{ label: 'Ran on', text: n.model ? `${m.ran} \u00b7 ${n.model}` : 'not recorded' },
|
|
116
|
+
{ label: 'Asked for', text: n.modelAsked ? `${n.modelAsked}${m.differs ? ' \u2014 differs from what ran' : ''}` : 'no model named (the agent\'s default)' },
|
|
117
|
+
];
|
|
118
|
+
// The family is the word the choice is made in (haiku, sonnet, opus); a model with none keeps its own name.
|
|
119
|
+
const short = (model) => (model ? (modelFamily(model) ?? modelName(model).toLowerCase()) : 'no model');
|
|
120
|
+
if (n.firstTry) {
|
|
121
|
+
lines.push({
|
|
122
|
+
label: 'Model gate',
|
|
123
|
+
text: `first asked: ${short(n.firstTry.asked)} \u2192 refused${n.firstTry.floor ? ` (floor: ${n.firstTry.floor})` : ''} \u2192 ${short(n.modelAsked ?? n.model)}`,
|
|
124
|
+
note: n.firstTry.text,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const tagOf = (id) => { const o = (nodes ?? []).find((x) => x.id === id); return o?.order != null ? ` (#${o.order})` : ''; };
|
|
128
|
+
if (n.escalatedFrom) {
|
|
129
|
+
lines.push({ label: 'Escalated', text: `escalated: ${short(n.escalatedFrom.from)} \u2192 ${short(n.escalatedFrom.to)}, after${tagOf(n.escalatedFrom.id)} closed "confidence: low"`, goto: n.escalatedFrom.id });
|
|
130
|
+
}
|
|
131
|
+
if (n.escalatedTo) lines.push({ label: 'Escalated', text: `closed "confidence: low"; repeated one model up${tagOf(n.escalatedTo)}`, goto: n.escalatedTo });
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
|
|
103
135
|
/** Who handed this agent its work: the node it hangs from. */
|
|
104
136
|
export function delegatedBy(n, nodes) {
|
|
105
137
|
const p = (nodes ?? []).find((x) => x.id === n.parentId) ?? null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Which key presses are shortcuts.
|
|
2
|
+
//
|
|
3
|
+
// The panel's shortcuts are single letters, and a single letter is also what someone types into a field, and what
|
|
4
|
+
// the browser's own shortcuts are made of once a modifier is held: Cmd+R reloads, Ctrl+F finds. A letter is a
|
|
5
|
+
// shortcut here only when it is neither.
|
|
6
|
+
|
|
7
|
+
/** Is the press going into something that takes text? */
|
|
8
|
+
export function typingIn(target) {
|
|
9
|
+
if (!target) return false;
|
|
10
|
+
const tag = String(target.tagName ?? '').toUpperCase();
|
|
11
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable === true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The key of a press that is a shortcut, or null.
|
|
16
|
+
*
|
|
17
|
+
* Null while Cmd or Ctrl is held: those belong to the browser and the system. Null while the press is going into a
|
|
18
|
+
* field. Alt is different, because on many keyboards it is how a symbol is typed at all: on a Turkish Mac "[" is
|
|
19
|
+
* Option+8 and "]" is Option+9, and the key the browser reports is the symbol. So Alt refuses a letter (Alt+R is
|
|
20
|
+
* not "r") and a named key, and lets a symbol through as the key it produced.
|
|
21
|
+
*/
|
|
22
|
+
export function shortcutOf(e) {
|
|
23
|
+
if (!e || e.metaKey || e.ctrlKey) return null;
|
|
24
|
+
if (typingIn(e.target)) return null;
|
|
25
|
+
if (typeof e.key !== 'string') return null;
|
|
26
|
+
if (e.altKey && (e.key.length !== 1 || LETTER.test(e.key))) return null;
|
|
27
|
+
return e.key;
|
|
28
|
+
}
|
|
29
|
+
const LETTER = /^\p{L}$/u;
|
|
@@ -48,8 +48,9 @@ function rowOf(n, now) {
|
|
|
48
48
|
* @param queue the approval queue (every session started here), as approvals.js orders it
|
|
49
49
|
* @param current the session being looked at
|
|
50
50
|
* @param folded Map sectionKey -> boolean, the viewer's own choices; a section not in it takes its default
|
|
51
|
+
* @param all true after "Expand all", false after "Fold all": what a section not in `folded` is
|
|
51
52
|
*/
|
|
52
|
-
export function sections(nodes, { queue = [], current = null, folded = new Map(), now = 0 } = {}) {
|
|
53
|
+
export function sections(nodes, { queue = [], current = null, folded = new Map(), all = null, now = 0 } = {}) {
|
|
53
54
|
const agents = (nodes ?? []).filter((n) => n.kind === 'agent');
|
|
54
55
|
const out = [];
|
|
55
56
|
|
|
@@ -89,7 +90,9 @@ export function sections(nodes, { queue = [], current = null, folded = new Map()
|
|
|
89
90
|
if (!s.rows.length) continue;
|
|
90
91
|
out.push({
|
|
91
92
|
key: s.key, title: s.title, count: s.rows.length, tone: s.tone, cards: [], rows: s.rows,
|
|
92
|
-
|
|
93
|
+
// As the viewer left it; untouched, it follows the last "Expand all" or "Fold all" (`all`), and before
|
|
94
|
+
// either its own default.
|
|
95
|
+
folded: folded.has(s.key) ? folded.get(s.key) : (typeof all === 'boolean' ? !all : s.foldedByDefault), foldable: true,
|
|
93
96
|
});
|
|
94
97
|
}
|
|
95
98
|
return out;
|
|
@@ -97,12 +100,14 @@ export function sections(nodes, { queue = [], current = null, folded = new Map()
|
|
|
97
100
|
|
|
98
101
|
/** What a request card says under the agent's name. */
|
|
99
102
|
export function wants(item) {
|
|
103
|
+
if (item.ask?.kind === 'questions') return 'asks';
|
|
104
|
+
if (item.ask?.kind === 'plan') return 'wants its plan approved';
|
|
100
105
|
return `wants to run ${item.toolName}`;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
/** Which view a window opens on when the viewer has not chosen one: the List on a phone, the graph elsewhere. */
|
|
104
109
|
export function defaultView(stored, phone) {
|
|
105
|
-
if (stored === 'graph' || stored === 'timeline' || stored === 'list') return stored;
|
|
110
|
+
if (stored === 'graph' || stored === 'timeline' || stored === 'list' || stored === 'models') return stored;
|
|
106
111
|
return phone ? 'list' : 'graph';
|
|
107
112
|
}
|
|
108
113
|
|