crewforth 3.0.3 → 3.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +211 -0
- package/README.md +3 -3
- package/VERSION +1 -1
- package/adopt.sh +86 -34
- package/kit/CLAUDE.md +1 -1
- package/kit/README.md +1 -1
- package/kit/agents/crew-commit-agent.md +7 -3
- package/kit/agents/crew-frontend-expert.md +4 -4
- package/kit/agents/crew-performance-expert.md +1 -1
- package/kit/agents/crew-review-agent.md +1 -1
- package/kit/eval/doctor.sh +57 -3
- package/kit/eval/golden-routing.txt +13 -0
- package/kit/eval/lib/git-bash.sh +11 -2
- package/kit/eval/parser-conformance.sh +1 -0
- package/kit/eval/routing-eval.sh +7 -3
- package/kit/eval/smoke-test.sh +2545 -214
- package/kit/hooks/commit-msg +1 -1
- package/kit/hooks/context-usage.sh +7 -1
- package/kit/hooks/guard-bash.sh +1608 -103
- package/kit/hooks/guard-commit-scan.sh +138 -19
- package/kit/hooks/guard-powershell.sh +89 -15
- package/kit/hooks/guard-write.sh +98 -22
- package/kit/hooks/no-bash-guard.ps1 +71 -0
- package/kit/hooks/pre-commit +5 -2
- package/kit/hooks/prompt-approval.sh +484 -0
- package/kit/hooks/route-hint.sh +13 -5
- package/kit/hooks/session-rehydrate.sh +18 -0
- package/kit/hooks/skill-trust.sh +17 -2
- package/kit/owned-blobs.tsv +43 -0
- package/kit/settings.json +20 -4
- package/kit/skills/a11y/SKILL.md +1 -0
- package/kit/skills/crew-doctor/SKILL.md +2 -2
- package/kit/skills/frontend/SKILL.md +3 -3
- package/kit/skills/frontend-flutter/SKILL.md +94 -0
- package/kit/skills/frontend-flutter/references/adaptive-and-accessible.md +55 -0
- package/kit/skills/frontend-flutter/references/platform-async-release.md +59 -0
- package/kit/skills/frontend-flutter/references/rebuilds-and-performance.md +36 -0
- package/kit/studio/server/hooks/studio-gate.sh +35 -1
- package/kit/studio/server/index.js +14 -4
- package/kit/studio/server/lib/graph.js +64 -3
- package/kit/studio/server/lib/kit.js +16 -2
- package/kit/studio/server/lib/permissions.js +77 -2
- package/kit/studio/server/lib/projects.js +60 -7
- package/kit/studio/server/lib/session.js +16 -2
- package/kit/studio/web/app.js +1765 -573
- package/kit/studio/web/approvals.js +124 -0
- package/kit/studio/web/canvas.js +932 -885
- package/kit/studio/web/chat.js +322 -145
- package/kit/studio/web/convo.js +174 -0
- package/kit/studio/web/dock.js +221 -0
- package/kit/studio/web/graph-plan.js +457 -0
- package/kit/studio/web/icons/builtin.svg +1 -0
- package/kit/studio/web/index.html +116 -50
- package/kit/studio/web/inspect.js +110 -0
- package/kit/studio/web/list-plan.js +112 -0
- package/kit/studio/web/list.js +157 -0
- package/kit/studio/web/liveness.js +66 -0
- package/kit/studio/web/nav.js +237 -0
- package/kit/studio/web/newsession.js +235 -0
- package/kit/studio/web/press.js +65 -0
- package/kit/studio/web/style.css +1281 -1014
- package/kit/studio/web/theme.js +43 -0
- package/kit/studio/web/timeline-plan.js +291 -0
- package/kit/studio/web/timeline.js +400 -0
- package/kit/studio/web/tokens.css +131 -0
- package/kit/studio/web/toolbar-fit.js +35 -0
- package/package.json +5 -4
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// The Timeline: the session's agents against time.
|
|
2
|
+
//
|
|
3
|
+
// Which rows there are, where a bar starts and ends and what it says is decided in timeline-plan.js. This file
|
|
4
|
+
// draws that, and hands back what the viewer did: a row chosen, a group folded, the view moved.
|
|
5
|
+
import {
|
|
6
|
+
rows, groupsOf, extent, windowOf, place, ticks, clock, stepRange, waitNote, factsOf, xOf, RANGES, DEFAULT_RANGE,
|
|
7
|
+
} from './timeline-plan.js';
|
|
8
|
+
import { Press } from './press.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
function mk(tag, cls, text) {
|
|
12
|
+
const n = document.createElement(tag);
|
|
13
|
+
if (cls) n.className = cls;
|
|
14
|
+
if (text != null) n.textContent = text;
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Timeline {
|
|
19
|
+
/**
|
|
20
|
+
* @param hooks.tileOf(node) the agent's mark, as the graph draws it
|
|
21
|
+
* @param hooks.statusOf(node) the word and tone of its status
|
|
22
|
+
* @param hooks.detailOf(node) its transcript's reading, or null while it is not read
|
|
23
|
+
* @param hooks.onSelect(node|null)
|
|
24
|
+
* @param hooks.onShowOnGraph(node)
|
|
25
|
+
* @param hooks.onOpenConversation()
|
|
26
|
+
* @param hooks.onChange(state) the toolbar's values changed
|
|
27
|
+
* @param hooks.fmt { duration(ms), tokens(n) }
|
|
28
|
+
*/
|
|
29
|
+
constructor(root, hooks = {}) {
|
|
30
|
+
this.root = root;
|
|
31
|
+
this.hooks = hooks;
|
|
32
|
+
// A redraw waits while a button is down on the view: the element under a press has to live to the release.
|
|
33
|
+
this.press = new Press(() => this.render());
|
|
34
|
+
if (typeof window !== 'undefined' && root.addEventListener) this.press.watch(root, window);
|
|
35
|
+
this.nodes = [];
|
|
36
|
+
this.session = null;
|
|
37
|
+
this.group = 'run';
|
|
38
|
+
this.range = DEFAULT_RANGE;
|
|
39
|
+
this.follow = true;
|
|
40
|
+
this.end = null; // where the view ends when it is not following
|
|
41
|
+
this.folded = new Set();
|
|
42
|
+
this.expanded = new Set();
|
|
43
|
+
this.waiting = new Set();
|
|
44
|
+
this.filter = null; // Set of statuses, or null
|
|
45
|
+
this.keepWaiting = false;
|
|
46
|
+
this.owned = null; // the session's summary when Studio started it
|
|
47
|
+
this.live = false;
|
|
48
|
+
this.selected = null;
|
|
49
|
+
this.now = () => Date.now();
|
|
50
|
+
this.win = null;
|
|
51
|
+
this.#build();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#build() {
|
|
55
|
+
this.root.classList.add('tl');
|
|
56
|
+
this.axis = mk('div', 'tl-axis');
|
|
57
|
+
this.axisLabel = mk('div', 'tl-axis-label', 'Agent');
|
|
58
|
+
this.axisTrack = mk('div', 'tl-axis-track');
|
|
59
|
+
this.axis.append(this.axisLabel, this.axisTrack);
|
|
60
|
+
|
|
61
|
+
this.body = mk('div', 'tl-body');
|
|
62
|
+
this.rowsEl = mk('div', 'tl-rows');
|
|
63
|
+
this.nowLine = mk('div', 'tl-now');
|
|
64
|
+
this.nowLine.hidden = true;
|
|
65
|
+
this.body.append(this.rowsEl, this.nowLine);
|
|
66
|
+
|
|
67
|
+
this.noteEl = mk('div', 'tl-note');
|
|
68
|
+
this.drawer = mk('div', 'tl-drawer');
|
|
69
|
+
this.drawer.setAttribute('role', 'region');
|
|
70
|
+
this.drawer.setAttribute('aria-label', 'Selected agent');
|
|
71
|
+
this.drawer.hidden = true;
|
|
72
|
+
this.empty = mk('div', 'tl-empty');
|
|
73
|
+
this.empty.hidden = true;
|
|
74
|
+
// Said when the stretch of time on screen holds no bar although the session has some: an empty track with
|
|
75
|
+
// no word would read as a session in which nothing happened.
|
|
76
|
+
this.away = mk('div', 'tl-away');
|
|
77
|
+
this.away.hidden = true;
|
|
78
|
+
const whole = mk('button', 'btn sm', 'Show the whole session');
|
|
79
|
+
whole.type = 'button';
|
|
80
|
+
whole.addEventListener('click', () => { this.range = 'all'; this.render(); this.#changed(); });
|
|
81
|
+
this.awayText = mk('span', null);
|
|
82
|
+
this.away.append(this.awayText, whole);
|
|
83
|
+
|
|
84
|
+
this.root.replaceChildren(this.axis, this.body, this.empty, this.away, this.noteEl, this.drawer);
|
|
85
|
+
|
|
86
|
+
// Moving the view by hand is the viewer saying "stay here": following stops.
|
|
87
|
+
this.body.addEventListener('wheel', (e) => {
|
|
88
|
+
const dx = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey ? e.deltaY : 0);
|
|
89
|
+
if (!dx || !this.win) return;
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
this.pan(dx);
|
|
92
|
+
}, { passive: false });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
state() {
|
|
96
|
+
return { range: this.range, follow: this.follow && this.live, live: this.live, canZoomIn: stepRange(this.range, -1) !== null, canZoomOut: stepRange(this.range, 1) !== null };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#changed() { this.hooks.onChange?.(this.state()); }
|
|
100
|
+
|
|
101
|
+
setSession(id) {
|
|
102
|
+
if (id === this.session) return;
|
|
103
|
+
this.session = id;
|
|
104
|
+
this.nodes = [];
|
|
105
|
+
this.folded = new Set();
|
|
106
|
+
this.expanded = new Set();
|
|
107
|
+
this.selected = null;
|
|
108
|
+
this.follow = true;
|
|
109
|
+
this.end = null;
|
|
110
|
+
this.render();
|
|
111
|
+
this.#changed();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setNodes(nodes) { this.nodes = nodes ?? []; this.render(); }
|
|
115
|
+
setGroup(g) { if (g === this.group) return; this.group = g; this.folded = new Set(); this.expanded = new Set(); this.render(); }
|
|
116
|
+
setWaiting(ids) { this.waiting = new Set(ids ?? []); this.render(); }
|
|
117
|
+
setFilter(statuses, { keepWaiting = false } = {}) { this.filter = statuses ? new Set(statuses) : null; this.keepWaiting = keepWaiting; this.render(); }
|
|
118
|
+
setOwned(summary) { this.owned = summary ?? null; this.render(); }
|
|
119
|
+
setLive(live) { if (live === this.live) return; this.live = live; this.render(); this.#changed(); }
|
|
120
|
+
|
|
121
|
+
zoom(by) {
|
|
122
|
+
const next = stepRange(this.range, by);
|
|
123
|
+
if (!next) return;
|
|
124
|
+
this.range = next;
|
|
125
|
+
this.render();
|
|
126
|
+
this.#changed();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setFollow(on) {
|
|
130
|
+
this.follow = on;
|
|
131
|
+
if (on) this.end = null;
|
|
132
|
+
this.render();
|
|
133
|
+
this.#changed();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Move the view by pixels. Following stops: the viewer has chosen where to look. */
|
|
137
|
+
pan(dx) {
|
|
138
|
+
const width = this.trackWidth();
|
|
139
|
+
if (!this.win || !width) return;
|
|
140
|
+
this.end = this.win.to + (dx / width) * this.win.span;
|
|
141
|
+
if (this.follow) { this.follow = false; this.#changed(); }
|
|
142
|
+
this.render();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Choose an agent from outside (the attention strip, the dock, a card in the conversation). A folded group
|
|
146
|
+
* that holds it is opened, and a group cut short shows all its rows: choosing something never hides it. */
|
|
147
|
+
select(id) {
|
|
148
|
+
this.selected = id;
|
|
149
|
+
if (id) {
|
|
150
|
+
for (const g of groupsOf(this.nodes, this.group)) {
|
|
151
|
+
if (!g.members.some((m) => m.id === id)) continue;
|
|
152
|
+
this.folded.delete(g.id);
|
|
153
|
+
this.expanded.add(g.id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.render();
|
|
157
|
+
if (id) this.rowsEl.querySelector?.('.tl-row.on')?.scrollIntoView?.({ block: 'nearest' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
dimmed(n) {
|
|
161
|
+
if (!this.filter) return false;
|
|
162
|
+
if (this.keepWaiting && this.waiting.has(n.id)) return false;
|
|
163
|
+
return !this.filter.has(n.status);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The label column's width is the stylesheet's (it narrows on a phone); the track is what is left. */
|
|
167
|
+
labelWidth() { return this.axisLabel.getBoundingClientRect?.().width ?? 0; }
|
|
168
|
+
|
|
169
|
+
trackWidth() {
|
|
170
|
+
const w = this.root.getBoundingClientRect?.().width ?? 0;
|
|
171
|
+
return Math.max(0, w - this.labelWidth());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Called once a second while the session is live, so that now moves. */
|
|
175
|
+
tick() { if (this.live && !this.root.hidden) this.render(); }
|
|
176
|
+
|
|
177
|
+
render() {
|
|
178
|
+
if (this.root.hidden) return;
|
|
179
|
+
if (this.press.defer()) return;
|
|
180
|
+
const now = this.now();
|
|
181
|
+
const agents = this.nodes.filter((n) => n.kind === 'agent');
|
|
182
|
+
const ext = extent(this.nodes, now, this.live);
|
|
183
|
+
const width = this.trackWidth();
|
|
184
|
+
|
|
185
|
+
this.empty.hidden = agents.length > 0;
|
|
186
|
+
this.body.hidden = agents.length === 0;
|
|
187
|
+
this.axis.hidden = agents.length === 0;
|
|
188
|
+
if (!agents.length) {
|
|
189
|
+
this.empty.replaceChildren(mk('strong', null, 'No agents yet'),
|
|
190
|
+
mk('span', null, 'This session has not delegated to a subagent. When it does, each one gets a row here.'));
|
|
191
|
+
this.noteEl.hidden = true;
|
|
192
|
+
this.nowLine.hidden = true;
|
|
193
|
+
this.away.hidden = true;
|
|
194
|
+
this.paintDrawer();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.win = windowOf(ext, this.range, { follow: this.follow && this.live, end: this.end });
|
|
199
|
+
const win = this.win;
|
|
200
|
+
|
|
201
|
+
// The axis.
|
|
202
|
+
this.axisTrack.replaceChildren(...(win ? ticks(win, width).map((t) => {
|
|
203
|
+
const m = mk('span', 'tl-tick', clock(t.at, t.seconds));
|
|
204
|
+
m.style.left = `${xOf(t.at, win, width)}px`;
|
|
205
|
+
return m;
|
|
206
|
+
}) : []));
|
|
207
|
+
|
|
208
|
+
// The rows.
|
|
209
|
+
// The rows are drawn again from the data each time; a keyboard reader's place among them is kept.
|
|
210
|
+
const focused = this.rowsEl.contains?.(document.activeElement) ? document.activeElement.dataset?.key ?? null : null;
|
|
211
|
+
const top = this.body.scrollTop;
|
|
212
|
+
const list = rows(this.nodes, {
|
|
213
|
+
group: this.group, folded: this.folded, expanded: this.expanded, waiting: this.waiting,
|
|
214
|
+
approvals: this.owned?.approvals ?? null, now,
|
|
215
|
+
});
|
|
216
|
+
this.rowsEl.replaceChildren(...list.map((r) => this.#row(r, win, width)));
|
|
217
|
+
this.body.scrollTop = top;
|
|
218
|
+
if (focused) [...this.rowsEl.querySelectorAll('[data-key]')].find((el) => el.dataset.key === focused)?.focus();
|
|
219
|
+
|
|
220
|
+
// Now: a line across the rows, while the session is live and now is on screen.
|
|
221
|
+
const nowX = win ? xOf(now, win, width) : -1;
|
|
222
|
+
this.nowLine.hidden = !this.live || !win || nowX < 0 || nowX > width + 1;
|
|
223
|
+
if (!this.nowLine.hidden) {
|
|
224
|
+
this.nowLine.style.left = `${this.labelWidth() + Math.min(nowX, width - 1)}px`;
|
|
225
|
+
this.nowLine.dataset.label = `now ${clock(now)}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Bars exist, and none of them is in the stretch on screen.
|
|
229
|
+
const timed = agents.filter((n) => n.startedAt != null);
|
|
230
|
+
const onScreen = this.rowsEl.querySelectorAll('.tl-bar').length;
|
|
231
|
+
this.away.hidden = !(timed.length && onScreen === 0);
|
|
232
|
+
if (!this.away.hidden) {
|
|
233
|
+
const last = Math.max(...timed.map((n) => n.endedAt ?? n.updatedAt ?? n.startedAt));
|
|
234
|
+
this.awayText.textContent = last < win.from
|
|
235
|
+
? `Nothing in this stretch of time. The last activity ended at ${clock(last)}.`
|
|
236
|
+
: 'Nothing in this stretch of time.';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const note = waitNote(this.owned);
|
|
240
|
+
this.noteEl.hidden = false;
|
|
241
|
+
this.noteEl.textContent = note.text;
|
|
242
|
+
this.noteEl.dataset.measured = String(note.measured);
|
|
243
|
+
|
|
244
|
+
this.paintDrawer();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#bars(track, laneList, win, width) {
|
|
248
|
+
const thin = laneList.length > 1;
|
|
249
|
+
laneList.forEach((lane, li) => {
|
|
250
|
+
for (const seg of lane) {
|
|
251
|
+
const at = win ? place(seg, win, width) : null;
|
|
252
|
+
if (!at) continue;
|
|
253
|
+
const b = mk('button', `tl-bar${thin ? ' thin' : ''}`);
|
|
254
|
+
b.type = 'button';
|
|
255
|
+
b.dataset.tone = seg.tone;
|
|
256
|
+
b.dataset.lane = String(li);
|
|
257
|
+
b.dataset.key = `bar:${seg.id}:${seg.from}`;
|
|
258
|
+
if (at.cutLeft) b.dataset.cutLeft = 'true';
|
|
259
|
+
if (at.cutRight) b.dataset.cutRight = 'true';
|
|
260
|
+
b.style.left = `${at.left}px`;
|
|
261
|
+
b.style.width = `${at.width}px`;
|
|
262
|
+
const node = this.nodes.find((n) => n.id === seg.id);
|
|
263
|
+
b.setAttribute('aria-label', `${node?.agentType ?? 'agent'}, ${seg.tone === 'wait' ? 'waiting for you' : (node?.status ?? '')}, ${clock(seg.from)} to ${clock(seg.to)}`);
|
|
264
|
+
if (node && this.dimmed(node)) b.classList.add('dim');
|
|
265
|
+
if (seg.id === this.selected) b.classList.add('on');
|
|
266
|
+
b.addEventListener('click', (e) => { e.stopPropagation(); this.#choose(seg.id); });
|
|
267
|
+
track.append(b);
|
|
268
|
+
// Where a failed agent stopped is marked.
|
|
269
|
+
if (seg.failedAt != null && !at.cutRight) {
|
|
270
|
+
const x = mk('span', 'tl-x', '×');
|
|
271
|
+
x.style.left = `${at.left + at.width}px`;
|
|
272
|
+
x.dataset.lane = String(li);
|
|
273
|
+
track.append(x);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#row(r, win, width) {
|
|
280
|
+
if (r.kind === 'more') {
|
|
281
|
+
const row = mk('div', 'tl-row tl-more');
|
|
282
|
+
const b = mk('button', 'tl-more-btn', `Show ${r.count} more in this ${this.group === 'run' ? 'run' : 'group'}`);
|
|
283
|
+
b.type = 'button';
|
|
284
|
+
b.addEventListener('click', () => { this.expanded.add(r.group); this.render(); });
|
|
285
|
+
const lab = mk('div', 'tl-lab');
|
|
286
|
+
if (r.indent) lab.classList.add('indent');
|
|
287
|
+
lab.append(b);
|
|
288
|
+
row.append(lab);
|
|
289
|
+
return row;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const row = mk('div', `tl-row tl-${r.kind}`);
|
|
293
|
+
const lab = mk(r.kind === 'merged' ? 'div' : 'button', 'tl-lab');
|
|
294
|
+
if (r.indent) lab.classList.add('indent');
|
|
295
|
+
lab.dataset.key = `row:${r.id}`;
|
|
296
|
+
const track = mk('div', 'tl-track');
|
|
297
|
+
|
|
298
|
+
if (r.kind === 'group') {
|
|
299
|
+
lab.type = 'button';
|
|
300
|
+
lab.setAttribute('aria-expanded', String(!r.folded));
|
|
301
|
+
const chev = mk('span', 'tl-chev');
|
|
302
|
+
chev.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg>';
|
|
303
|
+
lab.append(chev, mk('span', 'tl-name', r.label), mk('span', 'row-fill'), mk('span', 'sub', r.sub));
|
|
304
|
+
lab.addEventListener('click', () => {
|
|
305
|
+
if (this.folded.has(r.id)) this.folded.delete(r.id); else this.folded.add(r.id);
|
|
306
|
+
this.render();
|
|
307
|
+
});
|
|
308
|
+
this.#bars(track, r.lanes, win, width);
|
|
309
|
+
} else if (r.kind === 'merged') {
|
|
310
|
+
const tile = this.hooks.tileOf?.(r.members[0]);
|
|
311
|
+
if (tile) lab.append(tile);
|
|
312
|
+
lab.append(mk('span', 'tl-name', r.label), mk('span', 'row-fill'), mk('span', 'sub', r.sub));
|
|
313
|
+
this.#bars(track, r.lanes, win, width);
|
|
314
|
+
if (r.members.every((m) => this.dimmed(m))) row.classList.add('dim');
|
|
315
|
+
} else {
|
|
316
|
+
lab.type = 'button';
|
|
317
|
+
const tile = this.hooks.tileOf?.(r.node);
|
|
318
|
+
if (tile) lab.append(tile);
|
|
319
|
+
const sub = mk('span', 'sub', r.sub);
|
|
320
|
+
if (r.tone) sub.dataset.tone = r.tone;
|
|
321
|
+
lab.append(mk('span', 'tl-name', r.label), mk('span', 'row-fill'), sub);
|
|
322
|
+
lab.title = r.node.description ?? '';
|
|
323
|
+
// One listener, on the row: the label is a button inside it, and its click arrives here too. Two
|
|
324
|
+
// listeners chose the agent and un-chose it in the same click.
|
|
325
|
+
row.addEventListener('click', () => this.#choose(r.id));
|
|
326
|
+
if (r.timed) this.#bars(track, [r.bars], win, width);
|
|
327
|
+
// No start was recorded, so there is nowhere to put a bar; the row says that instead of drawing one at zero.
|
|
328
|
+
else track.append(mk('span', 'tl-untimed', 'no start time recorded'));
|
|
329
|
+
if (r.id === this.selected) row.classList.add('on');
|
|
330
|
+
if (this.dimmed(r.node)) row.classList.add('dim');
|
|
331
|
+
}
|
|
332
|
+
row.append(lab, track);
|
|
333
|
+
return row;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#choose(id) {
|
|
337
|
+
this.selected = this.selected === id ? null : id;
|
|
338
|
+
this.render();
|
|
339
|
+
this.hooks.onSelect?.(this.selected ? this.nodes.find((n) => n.id === this.selected) ?? null : null);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The drawer under the rows: the chosen agent, its task, its numbers, and the last thing that went wrong. */
|
|
343
|
+
paintDrawer() {
|
|
344
|
+
const n = this.selected ? this.nodes.find((x) => x.id === this.selected) : null;
|
|
345
|
+
this.drawer.hidden = !n;
|
|
346
|
+
if (!n) return;
|
|
347
|
+
const now = this.now();
|
|
348
|
+
const st = this.hooks.statusOf?.(n) ?? { word: n.status, tone: null };
|
|
349
|
+
|
|
350
|
+
const left = mk('div', 'tl-d-main');
|
|
351
|
+
const head = mk('div', 'tl-d-head');
|
|
352
|
+
const tile = this.hooks.tileOf?.(n);
|
|
353
|
+
if (tile) head.append(tile);
|
|
354
|
+
const pill = mk('span', 'pill');
|
|
355
|
+
const dot = mk('span', 'dot');
|
|
356
|
+
dot.dataset.tone = st.tone ?? 'none';
|
|
357
|
+
pill.append(dot, mk('span', null, st.word ?? 'State not measured'));
|
|
358
|
+
head.append(mk('span', 'tl-d-name', n.agentType ?? 'unknown agent'), pill);
|
|
359
|
+
left.append(head, mk('div', 'tl-d-task', n.description ?? ''), mk('div', 'sub tl-d-facts', factsOf(n, now, this.hooks.fmt)));
|
|
360
|
+
|
|
361
|
+
const mid = mk('div', 'tl-d-error');
|
|
362
|
+
mid.append(mk('div', 'isec-h', 'Last error'));
|
|
363
|
+
const detail = this.hooks.detailOf?.(n);
|
|
364
|
+
if (!detail || detail.loading) {
|
|
365
|
+
mid.append(mk('div', 'ihint', 'Reading the agent transcript…'));
|
|
366
|
+
} else if (detail.measured === false) {
|
|
367
|
+
const hint = mk('div', 'ihint');
|
|
368
|
+
hint.append(mk('strong', null, 'Not measured'), mk('div', null, detail.reason || 'no reason given'));
|
|
369
|
+
mid.append(hint);
|
|
370
|
+
} else if (!detail.lastError) {
|
|
371
|
+
mid.append(mk('div', 'ihint', 'None — no tool call in this agent\'s transcript came back as an error.'));
|
|
372
|
+
} else {
|
|
373
|
+
const e = detail.lastError;
|
|
374
|
+
const text = mk('pre', 'tl-d-errtext', e.truncated ? `… ${(e.length - e.text.length).toLocaleString('en-US')} earlier characters not shown\n${e.text}` : (e.text || '(no output)'));
|
|
375
|
+
const count = detail.errors ?? 0;
|
|
376
|
+
const facts = [`${count} ${count === 1 ? 'error' : 'errors'}`];
|
|
377
|
+
if (e.at != null) facts.push(`last at ${clock(e.at, true)}`);
|
|
378
|
+
if (e.tool) facts.push(e.tool);
|
|
379
|
+
mid.append(text, mk('div', 'sub', facts.join(' · ')));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const acts = mk('div', 'tl-d-acts');
|
|
383
|
+
const show = mk('button', 'btn sm primary', 'Show on graph');
|
|
384
|
+
show.type = 'button';
|
|
385
|
+
show.addEventListener('click', () => this.hooks.onShowOnGraph?.(n));
|
|
386
|
+
const open = mk('button', 'btn sm', 'Open conversation');
|
|
387
|
+
open.type = 'button';
|
|
388
|
+
open.addEventListener('click', () => this.hooks.onOpenConversation?.());
|
|
389
|
+
const close = mk('button', 'btn icon sm ghost');
|
|
390
|
+
close.type = 'button';
|
|
391
|
+
close.setAttribute('aria-label', 'Close');
|
|
392
|
+
close.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
|
393
|
+
close.addEventListener('click', () => this.#choose(n.id));
|
|
394
|
+
acts.append(show, open, close);
|
|
395
|
+
|
|
396
|
+
this.drawer.replaceChildren(left, mid, acts);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export { RANGES };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* GENERATED from packaging/design/tokens.json (Crewforth, version 1) by
|
|
2
|
+
packaging/gen-studio-tokens.mjs. Do not edit: change the JSON and run the generator. */
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: dark;
|
|
6
|
+
--ground: #0B1020;
|
|
7
|
+
--surface: #141821;
|
|
8
|
+
--surface-raised: #1b202b;
|
|
9
|
+
--line: #262c39;
|
|
10
|
+
--ink: #e6e9f0;
|
|
11
|
+
--ink-dim: #9aa7cc;
|
|
12
|
+
--ink-faint: #5c6472;
|
|
13
|
+
--accent: #A78BFA;
|
|
14
|
+
--on-accent: #0B1020;
|
|
15
|
+
--wordmark-ink: #FFFFFF;
|
|
16
|
+
--wordmark-accent: #A78BFA;
|
|
17
|
+
--chevron-1: #E5E7FB;
|
|
18
|
+
--chevron-2: #B9BEF9;
|
|
19
|
+
--chevron-3: #A78BFA;
|
|
20
|
+
--brand-navy: #0B1020;
|
|
21
|
+
--studio-accent: #5b8cff;
|
|
22
|
+
--status-busy: #26c6e6;
|
|
23
|
+
--status-waiting: #f2a65a;
|
|
24
|
+
--status-idle: #9aa7cc;
|
|
25
|
+
--status-good: #35c874;
|
|
26
|
+
--status-bad: #ff8a4d;
|
|
27
|
+
--status-fail: #ff6b7a;
|
|
28
|
+
--font-sans: Inter, ui-sans-serif, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
29
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
30
|
+
--type-wordmark-size: 56px;
|
|
31
|
+
--type-wordmark-line: 1;
|
|
32
|
+
--type-wordmark-weight: 800;
|
|
33
|
+
--type-wordmark-tracking: -1.5px;
|
|
34
|
+
--type-hero-size: 48px;
|
|
35
|
+
--type-hero-line: 52px;
|
|
36
|
+
--type-hero-weight: 800;
|
|
37
|
+
--type-hero-tracking: -1px;
|
|
38
|
+
--type-h1-size: 36px;
|
|
39
|
+
--type-h1-line: 42px;
|
|
40
|
+
--type-h1-weight: 700;
|
|
41
|
+
--type-h1-tracking: -0.5px;
|
|
42
|
+
--type-h2-size: 24px;
|
|
43
|
+
--type-h2-line: 32px;
|
|
44
|
+
--type-h2-weight: 700;
|
|
45
|
+
--type-h3-size: 18px;
|
|
46
|
+
--type-h3-line: 26px;
|
|
47
|
+
--type-h3-weight: 600;
|
|
48
|
+
--type-body-size: 16px;
|
|
49
|
+
--type-body-line: 26px;
|
|
50
|
+
--type-body-weight: 400;
|
|
51
|
+
--type-body-sm-size: 14px;
|
|
52
|
+
--type-body-sm-line: 22px;
|
|
53
|
+
--type-body-sm-weight: 400;
|
|
54
|
+
--type-label-size: 12px;
|
|
55
|
+
--type-label-line: 16px;
|
|
56
|
+
--type-label-weight: 600;
|
|
57
|
+
--type-label-tracking: 0.06em;
|
|
58
|
+
--type-code-size: 14px;
|
|
59
|
+
--type-code-line: 22px;
|
|
60
|
+
--type-code-weight: 400;
|
|
61
|
+
--type-code-sm-size: 12px;
|
|
62
|
+
--type-code-sm-line: 18px;
|
|
63
|
+
--type-code-sm-weight: 400;
|
|
64
|
+
--space-1: 4px;
|
|
65
|
+
--space-2: 8px;
|
|
66
|
+
--space-3: 12px;
|
|
67
|
+
--space-4: 16px;
|
|
68
|
+
--space-6: 24px;
|
|
69
|
+
--space-8: 32px;
|
|
70
|
+
--space-12: 48px;
|
|
71
|
+
--space-16: 64px;
|
|
72
|
+
--radius-sm: 4px;
|
|
73
|
+
--radius-md: 10px;
|
|
74
|
+
--radius-lg: 20px;
|
|
75
|
+
--radius-icon: 23%;
|
|
76
|
+
--radius-pill: 999px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media (prefers-color-scheme: light) {
|
|
80
|
+
:root:not([data-theme="dark"]) {
|
|
81
|
+
color-scheme: light;
|
|
82
|
+
--ground: #f7f8fa;
|
|
83
|
+
--surface: #ffffff;
|
|
84
|
+
--surface-raised: #eef0f4;
|
|
85
|
+
--line: #d9dde5;
|
|
86
|
+
--ink: #1a1d24;
|
|
87
|
+
--ink-dim: #5c6472;
|
|
88
|
+
--ink-faint: #8d95a3;
|
|
89
|
+
--accent: #6D28D9;
|
|
90
|
+
--on-accent: #ffffff;
|
|
91
|
+
--wordmark-ink: #0F172A;
|
|
92
|
+
--wordmark-accent: #6366F1;
|
|
93
|
+
--chevron-1: #A8AEF6;
|
|
94
|
+
--chevron-2: #7C83F5;
|
|
95
|
+
--chevron-3: #6D28D9;
|
|
96
|
+
--brand-navy: #0B1020;
|
|
97
|
+
--studio-accent: #5b8cff;
|
|
98
|
+
--status-busy: #26c6e6;
|
|
99
|
+
--status-waiting: #f2a65a;
|
|
100
|
+
--status-idle: #9aa7cc;
|
|
101
|
+
--status-good: #35c874;
|
|
102
|
+
--status-bad: #ff8a4d;
|
|
103
|
+
--status-fail: #d92d3f;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
:root[data-theme="light"] {
|
|
108
|
+
color-scheme: light;
|
|
109
|
+
--ground: #f7f8fa;
|
|
110
|
+
--surface: #ffffff;
|
|
111
|
+
--surface-raised: #eef0f4;
|
|
112
|
+
--line: #d9dde5;
|
|
113
|
+
--ink: #1a1d24;
|
|
114
|
+
--ink-dim: #5c6472;
|
|
115
|
+
--ink-faint: #8d95a3;
|
|
116
|
+
--accent: #6D28D9;
|
|
117
|
+
--on-accent: #ffffff;
|
|
118
|
+
--wordmark-ink: #0F172A;
|
|
119
|
+
--wordmark-accent: #6366F1;
|
|
120
|
+
--chevron-1: #A8AEF6;
|
|
121
|
+
--chevron-2: #7C83F5;
|
|
122
|
+
--chevron-3: #6D28D9;
|
|
123
|
+
--brand-navy: #0B1020;
|
|
124
|
+
--studio-accent: #5b8cff;
|
|
125
|
+
--status-busy: #26c6e6;
|
|
126
|
+
--status-waiting: #f2a65a;
|
|
127
|
+
--status-idle: #9aa7cc;
|
|
128
|
+
--status-good: #35c874;
|
|
129
|
+
--status-bad: #ff8a4d;
|
|
130
|
+
--status-fail: #d92d3f;
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// How much the toolbar gives up so that what it shows fits in it.
|
|
2
|
+
//
|
|
3
|
+
// The steps used to be widths in the stylesheet: under 880 px the buttons lost their words, under 760 px the
|
|
4
|
+
// menus became one. Those numbers were a guess at how wide the words are, and the guess was low: with every word
|
|
5
|
+
// on, the Graph's toolbar needs about 1080 px in the font it was measured in, so between the two it ran out of
|
|
6
|
+
// its own box and over whatever was beside it. A width cannot be right for every font and every label. So the
|
|
7
|
+
// toolbar is measured instead: a step at a time, until it holds what it shows.
|
|
8
|
+
//
|
|
9
|
+
// 0 everything
|
|
10
|
+
// 1 the buttons that have an icon lose their words, and the note goes
|
|
11
|
+
// 2 Group, Density, Show, Expand and Fold become one menu, "View options"
|
|
12
|
+
// 3 the zoom figure and the name of the Timeline's range go; their buttons stay
|
|
13
|
+
|
|
14
|
+
export const FIT_MAX = 3;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The first step at which the toolbar holds what it shows.
|
|
18
|
+
* @param overflows (step) => boolean — puts the step on and says whether the toolbar still runs over
|
|
19
|
+
* @returns 0…max; max when nothing more can be given up, whether or not it fits
|
|
20
|
+
*/
|
|
21
|
+
export function fitLevel(overflows, max = FIT_MAX) {
|
|
22
|
+
for (let step = 0; step < max; step += 1) if (!overflows(step)) return step;
|
|
23
|
+
return max;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Does a row run over? Asked of the boxes, not of scrollWidth: a browser may leave the row's own end padding out
|
|
28
|
+
* of scrollWidth, and the last button would sit against the edge before anything was said.
|
|
29
|
+
* @param box { right, padRight } of the row
|
|
30
|
+
* @param ends the right edge of each thing shown in it
|
|
31
|
+
*/
|
|
32
|
+
export function runsOver(box, ends) {
|
|
33
|
+
const limit = box.right - (box.padRight ?? 0);
|
|
34
|
+
return ends.some((right) => right > limit + 0.5);
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewforth",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Crewforth
|
|
3
|
+
"version": "3.1.0-rc.1",
|
|
4
|
+
"description": "Crewforth — your engineering crew for Claude Code: agents, skills and tool-level gates, in any stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"crewforth": "bin/cli.js"
|
|
7
7
|
},
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"kit/",
|
|
16
16
|
"VERSION",
|
|
17
17
|
"LICENSE",
|
|
18
|
-
"README.md",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
21
22
|
"node": ">=20"
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"code-review",
|
|
43
44
|
"git-hooks"
|
|
44
45
|
],
|
|
45
|
-
"author": "
|
|
46
|
+
"author": "Barış Yerlikaya",
|
|
46
47
|
"repository": {
|
|
47
48
|
"type": "git",
|
|
48
49
|
"url": "git+https://github.com/Crewforth/crewforth.git"
|