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
|
@@ -4,82 +4,148 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Crewforth Studio</title>
|
|
7
|
+
<link rel="stylesheet" href="/tokens.css">
|
|
7
8
|
<link rel="stylesheet" href="/style.css">
|
|
8
9
|
</head>
|
|
9
10
|
<body>
|
|
10
11
|
<header class="bar">
|
|
11
|
-
<
|
|
12
|
+
<button id="menu-btn" class="btn icon ghost" type="button" aria-label="Open the navigator" aria-expanded="false">
|
|
13
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 4h11M2.5 8h11M2.5 12h11"/></svg>
|
|
14
|
+
</button>
|
|
15
|
+
<a id="home" class="brand" href="/" title="Back to the latest session">
|
|
12
16
|
<svg class="brand-mark" viewBox="0 0 200 200" aria-hidden="true">
|
|
13
17
|
<polyline points="44,58 86,100 44,142" stroke-width="21" class="bar-1"/>
|
|
14
18
|
<polyline points="80,58 122,100 80,142" stroke-width="21" class="bar-2"/>
|
|
15
19
|
<polyline points="116,58 158,100 116,142" stroke-width="24" class="bar-3"/>
|
|
16
20
|
</svg>
|
|
17
21
|
<h1>Crewforth <span class="brand-accent">Studio</span></h1>
|
|
18
|
-
</
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
</
|
|
22
|
+
</a>
|
|
23
|
+
<span class="bar-sep" aria-hidden="true"></span>
|
|
24
|
+
<nav id="crumb" class="crumb" aria-label="Breadcrumb"></nav>
|
|
25
|
+
<span class="bar-fill"></span>
|
|
26
|
+
<div id="graph-summary" class="chips" role="group" aria-label="Session summary"></div>
|
|
27
|
+
<span class="bar-sep" aria-hidden="true"></span>
|
|
28
|
+
<span id="pulse" class="live" role="status" data-state="connecting"><span class="dot"></span><strong>Connecting</strong><span class="live-detail"></span></span>
|
|
29
|
+
<button id="theme" class="btn icon ghost" type="button" aria-label="Switch theme">
|
|
30
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M13 9.5A5.5 5.5 0 0 1 6.5 3a5.5 5.5 0 1 0 6.5 6.5z"/></svg>
|
|
31
|
+
</button>
|
|
32
|
+
<button id="fullscreen" class="btn icon ghost" type="button" title="Full screen (f)" aria-label="Full screen">
|
|
33
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6V3h3M10 3h3v3M13 10v3h-3M6 13H3v-3"/></svg>
|
|
34
|
+
</button>
|
|
35
|
+
<button id="new-session" class="btn primary" type="button">
|
|
36
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg><span>New session</span>
|
|
37
|
+
</button>
|
|
31
38
|
</header>
|
|
32
39
|
|
|
33
|
-
<div class="shell">
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
</
|
|
65
|
-
|
|
66
|
-
|
|
40
|
+
<div class="shell no-chat">
|
|
41
|
+
<nav id="side-rail" class="side-rail" aria-label="Navigator, collapsed" hidden>
|
|
42
|
+
<button class="rail-btn" type="button" data-rail-tab="projects" aria-label="Projects">
|
|
43
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M2 4.5A1.5 1.5 0 0 1 3.5 3h3l1.5 1.5h4.5A1.5 1.5 0 0 1 14 6v5.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 11.5z"/></svg>
|
|
44
|
+
</button>
|
|
45
|
+
<button class="rail-btn" type="button" data-rail-tab="live" aria-label="Live sessions">
|
|
46
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M1.5 8h3l2-4 3 8 2-4h3"/></svg>
|
|
47
|
+
</button>
|
|
48
|
+
<button class="rail-btn" type="button" data-rail-tab="machines" aria-label="Machines">
|
|
49
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="8" rx="1.5"/><path d="M6 14h4M8 11v3"/></svg>
|
|
50
|
+
</button>
|
|
51
|
+
<span class="rail-fill"></span>
|
|
52
|
+
<button id="side-show" class="rail-btn" type="button" title="Expand the navigator (])" aria-label="Expand the navigator">
|
|
53
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg>
|
|
54
|
+
</button>
|
|
55
|
+
</nav>
|
|
56
|
+
|
|
57
|
+
<aside class="side" aria-label="Projects and sessions">
|
|
58
|
+
<div class="nav-top">
|
|
59
|
+
<label class="nav-search">
|
|
60
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></svg>
|
|
61
|
+
<input id="filter" type="search" placeholder="Search projects and sessions"
|
|
62
|
+
aria-label="Search projects and sessions" autocomplete="off" spellcheck="false">
|
|
63
|
+
</label>
|
|
64
|
+
<button id="side-hide" class="btn icon ghost sm" type="button" title="Collapse the navigator ([)" aria-label="Collapse the navigator">
|
|
65
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M10 4l-4 4 4 4"/></svg>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="seg" role="tablist" aria-label="Navigator">
|
|
69
|
+
<button class="seg-btn" type="button" role="tab" id="tab-projects" data-tab="projects" aria-controls="sessions" aria-selected="true">Projects</button>
|
|
70
|
+
<button class="seg-btn" type="button" role="tab" id="tab-live" data-tab="live" aria-controls="fleet" aria-selected="false">Live <span id="live-count" class="badge" hidden></span></button>
|
|
71
|
+
<button class="seg-btn" type="button" role="tab" id="tab-machines" data-tab="machines" aria-controls="reach" aria-selected="false">Machines</button>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="sessions" class="nav-list" role="tabpanel" aria-labelledby="tab-projects" aria-busy="true"><div class="skel" aria-hidden="true"><span></span><span></span><span></span><span></span></div></div>
|
|
74
|
+
<div id="fleet" class="nav-list" role="tabpanel" aria-labelledby="tab-live" hidden></div>
|
|
75
|
+
<div id="reach" class="nav-list" role="tabpanel" aria-labelledby="tab-machines" hidden></div>
|
|
76
|
+
<div id="other-machines" class="nav-foot" hidden></div>
|
|
67
77
|
</aside>
|
|
68
78
|
|
|
69
79
|
<div class="resizer" id="resizer" role="separator" aria-orientation="vertical"
|
|
70
80
|
aria-label="Resize sidebar" tabindex="0"></div>
|
|
71
81
|
|
|
72
82
|
<main class="stage">
|
|
83
|
+
<div id="phone-head" class="phone-head"></div>
|
|
84
|
+
<div class="toolbar" role="toolbar" aria-label="Canvas">
|
|
85
|
+
<div class="seg tb-views" role="tablist" aria-label="View">
|
|
86
|
+
<button id="view-graph" class="seg-btn" type="button" role="tab" aria-label="Graph view" aria-selected="true" aria-controls="canvas">
|
|
87
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><circle cx="3.5" cy="8" r="1.8"/><circle cx="12.5" cy="4" r="1.8"/><circle cx="12.5" cy="12" r="1.8"/><path d="M5.2 7.3L10.8 4.7M5.2 8.7l5.6 2.6"/></svg><span class="tb-words">Graph</span>
|
|
88
|
+
</button>
|
|
89
|
+
<button id="view-timeline" class="seg-btn" type="button" role="tab" aria-label="Timeline view" aria-selected="false" aria-controls="timeline">
|
|
90
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M2 4h7M5 8h9M3 12h6"/></svg><span class="tb-words">Timeline</span>
|
|
91
|
+
</button>
|
|
92
|
+
<button id="view-list" class="seg-btn" type="button" role="tab" aria-label="List view" aria-selected="false" aria-controls="list">
|
|
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
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
<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
|
+
<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>
|
|
100
|
+
<span id="foot-note" class="toolbar-note" role="status"></span>
|
|
101
|
+
<span class="toolbar-fill"></span>
|
|
102
|
+
<span class="tb-follow" data-view="timeline" hidden>
|
|
103
|
+
<span class="sub" aria-hidden="true">Follow now</span>
|
|
104
|
+
<button id="tb-follow" class="switch" type="button" role="switch" aria-checked="true" aria-label="Follow now"><span></span></button>
|
|
105
|
+
</span>
|
|
106
|
+
<button id="tb-range-out" class="btn icon sm ghost" type="button" data-view="timeline" aria-label="Show more time" hidden>
|
|
107
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h10"/></svg>
|
|
108
|
+
</button>
|
|
109
|
+
<span id="tb-range" class="tb-zoom tb-range" data-view="timeline" hidden>15m</span>
|
|
110
|
+
<button id="tb-range-in" class="btn icon sm ghost" type="button" data-view="timeline" aria-label="Show less time" hidden>
|
|
111
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg>
|
|
112
|
+
</button>
|
|
113
|
+
<button id="tb-expand" data-view="graph" class="btn sm" type="button" aria-label="Expand all groups">
|
|
114
|
+
<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
|
+
</button>
|
|
116
|
+
<button id="tb-fold" data-view="graph" class="btn sm" type="button" aria-label="Fold all groups">
|
|
117
|
+
<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
|
+
</button>
|
|
119
|
+
<button id="tb-zoom-out" data-view="graph" class="btn icon sm ghost" type="button" aria-label="Zoom out">
|
|
120
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h10"/></svg>
|
|
121
|
+
</button>
|
|
122
|
+
<button id="tb-zoom" data-view="graph" class="tb-zoom" type="button" title="Back to 100%" aria-label="Zoom 100%. Back to 100%">100%</button>
|
|
123
|
+
<button id="tb-zoom-in" data-view="graph" class="btn icon sm ghost" type="button" aria-label="Zoom in">
|
|
124
|
+
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg>
|
|
125
|
+
</button>
|
|
126
|
+
<button id="tb-fit" data-view="graph" class="btn sm" type="button" aria-label="Fit the graph to the view">
|
|
127
|
+
<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
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<div id="stage-note" class="stage-note" role="status" hidden></div>
|
|
131
|
+
<div id="attention" class="attention" role="region" aria-label="Needs attention" hidden></div>
|
|
73
132
|
<div id="canvas" class="canvas"></div>
|
|
74
|
-
<
|
|
133
|
+
<div id="timeline" role="tabpanel" aria-labelledby="view-timeline" hidden></div>
|
|
134
|
+
<div id="list" role="tabpanel" aria-labelledby="view-list" hidden></div>
|
|
135
|
+
<div id="first-run" class="first-run" hidden></div>
|
|
136
|
+
<div id="dock" role="region" aria-label="Waiting for approval" hidden></div>
|
|
75
137
|
</main>
|
|
76
138
|
|
|
139
|
+
<aside id="inspector" class="inspector" aria-label="Selected node" hidden></aside>
|
|
140
|
+
|
|
77
141
|
<div class="resizer" id="chat-split" role="separator" aria-orientation="vertical"
|
|
78
142
|
aria-label="Resize conversation" tabindex="0" hidden></div>
|
|
79
143
|
<section id="chat" class="chat" hidden></section>
|
|
144
|
+
<section id="new-panel" class="newpanel" aria-label="New session" hidden></section>
|
|
80
145
|
</div>
|
|
81
146
|
|
|
82
|
-
<
|
|
147
|
+
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
|
148
|
+
<div id="menu" class="menu" role="menu" hidden></div>
|
|
83
149
|
|
|
84
150
|
<script type="module" src="/app.js"></script>
|
|
85
151
|
</body>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// What the inspector says about the node that is selected.
|
|
2
|
+
//
|
|
3
|
+
// Functions of the server's data, with no page in them: the four tiles, what the agent is doing right now, which
|
|
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
|
+
// written "Not measured", with why — never as 0 and never as an empty list.
|
|
6
|
+
|
|
7
|
+
const NOT_MEASURED = 'Not measured';
|
|
8
|
+
|
|
9
|
+
export function fmtTokens(t) {
|
|
10
|
+
if (t == null) return null;
|
|
11
|
+
if (t < 1000) return `${t}`;
|
|
12
|
+
if (t < 1_000_000) return `${(t / 1000).toFixed(t < 10_000 ? 1 : 0)}k`;
|
|
13
|
+
return `${(t / 1_000_000).toFixed(2)}M`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function fmtDuration(ms) {
|
|
17
|
+
if (ms == null || !Number.isFinite(ms)) return null;
|
|
18
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
19
|
+
if (s < 60) return `${s}s`;
|
|
20
|
+
const m = Math.floor(s / 60);
|
|
21
|
+
return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** How long the agent has been at it: the recorded duration when it has ended, the clock since it started while
|
|
25
|
+
* it is still running, and null when neither was recorded. */
|
|
26
|
+
export function elapsed(n, nowMs) {
|
|
27
|
+
if (n.durationMs != null) return n.durationMs;
|
|
28
|
+
if (n.startedAt != null && (n.status === 'running' || n.status === 'starting')) return Math.max(0, nowMs - n.startedAt);
|
|
29
|
+
if (n.startedAt != null && n.endedAt != null) return n.endedAt - n.startedAt;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The four tiles. A count the transcript gave is a count, zero included; a figure it did not give says so.
|
|
35
|
+
*/
|
|
36
|
+
export function tiles(n, nowMs) {
|
|
37
|
+
const el = elapsed(n, nowMs);
|
|
38
|
+
return [
|
|
39
|
+
{ label: 'Tool calls', value: String(n.toolCount ?? 0), measured: n.toolCount != null },
|
|
40
|
+
{
|
|
41
|
+
label: 'Tokens',
|
|
42
|
+
value: n.tokens == null ? NOT_MEASURED : fmtTokens(n.tokens),
|
|
43
|
+
measured: n.tokens != null,
|
|
44
|
+
why: n.tokens == null ? 'the transcript carries no usage record for this agent yet' : null,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: 'Elapsed',
|
|
48
|
+
value: el == null ? NOT_MEASURED : fmtDuration(el),
|
|
49
|
+
measured: el != null,
|
|
50
|
+
why: el == null ? 'no start time was recorded' : null,
|
|
51
|
+
},
|
|
52
|
+
{ label: 'Errors', value: String(n.errors ?? 0), measured: n.errors != null, bad: (n.errors ?? 0) > 0 },
|
|
53
|
+
].map((t) => (t.measured ? t : { ...t, value: NOT_MEASURED, why: t.why ?? 'not in the transcript' }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** "8m 20s · started 14:02" */
|
|
57
|
+
export function timeLine(n, nowMs) {
|
|
58
|
+
const parts = [];
|
|
59
|
+
const el = elapsed(n, nowMs);
|
|
60
|
+
if (el != null) parts.push(fmtDuration(el));
|
|
61
|
+
if (n.startedAt != null) {
|
|
62
|
+
const d = new Date(n.startedAt);
|
|
63
|
+
parts.push(`started ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`);
|
|
64
|
+
}
|
|
65
|
+
return parts.join(' · ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The tool the agent is running, with what it is running it on. "Right now" while the agent works; "Last tool"
|
|
70
|
+
* once it has stopped. The command comes from the agent's transcript, which may not be read yet.
|
|
71
|
+
*/
|
|
72
|
+
export function rightNow(n, detail) {
|
|
73
|
+
const live = n.status === 'running' || n.status === 'starting';
|
|
74
|
+
const last = detail?.timeline?.length ? detail.timeline[detail.timeline.length - 1] : null;
|
|
75
|
+
const tool = last?.name ?? n.lastTool ?? null;
|
|
76
|
+
if (!tool) return { heading: live ? 'Right now' : 'Last tool', tool: null, text: live ? 'No tool call yet' : 'No tool was called' };
|
|
77
|
+
return { heading: live ? 'Right now' : 'Last tool', tool, text: last?.label ?? '' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The skills the agent invoked, in the order it first did, each once. */
|
|
81
|
+
export function skillsOf(detail) {
|
|
82
|
+
const seen = [];
|
|
83
|
+
for (const step of detail?.timeline ?? []) {
|
|
84
|
+
if (step.name !== 'Skill' || !step.label) continue;
|
|
85
|
+
if (!seen.includes(step.label)) seen.push(step.label);
|
|
86
|
+
}
|
|
87
|
+
return seen;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Who handed this agent its work: the node it hangs from. */
|
|
91
|
+
export function delegatedBy(n, nodes) {
|
|
92
|
+
const p = (nodes ?? []).find((x) => x.id === n.parentId) ?? null;
|
|
93
|
+
if (!p) return null;
|
|
94
|
+
if (p.kind === 'session') return { id: p.id, text: `Session${p.gitBranch ? ` · ${p.gitBranch}` : ''}` };
|
|
95
|
+
if (p.kind === 'workflow') return { id: p.id, text: `Workflow run · ${p.workflowId ?? p.id}` };
|
|
96
|
+
return { id: p.id, text: p.agentType ?? p.id };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The report, or the plain statement that there is none yet and why. */
|
|
100
|
+
export function reportOf(n, detail) {
|
|
101
|
+
if (detail?.report) return { text: detail.report, present: true };
|
|
102
|
+
const live = n.status === 'running' || n.status === 'starting';
|
|
103
|
+
return {
|
|
104
|
+
present: false,
|
|
105
|
+
text: live ? 'Not reported yet — the agent is still running.' : 'Not reported — the transcript holds no closing text.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const TABS = ['overview', 'conversation', 'gates', 'stats'];
|
|
110
|
+
export const TAB_WORD = { overview: 'Overview', conversation: 'Conversation', gates: 'Gates', stats: 'Stats' };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// What the List view shows: the session's agents by what they need from the reader, in a fixed order.
|
|
2
|
+
//
|
|
3
|
+
// Needs you, then Failed, then Running, then Done. The order never changes with the data, so the thing that
|
|
4
|
+
// needs someone is always at the top. A status this panel has no word for is not put into one of the four: it
|
|
5
|
+
// gets a section of its own, under its own name, after them.
|
|
6
|
+
import { stateOf } from './graph-plan.js';
|
|
7
|
+
|
|
8
|
+
const typeOf = (n) => n.agentType ?? 'unknown agent';
|
|
9
|
+
|
|
10
|
+
/** "8m", "1h 4m", "40s": how long, in as little room as a row has. */
|
|
11
|
+
export function short(ms) {
|
|
12
|
+
if (ms == null || !Number.isFinite(ms) || ms < 0) return null;
|
|
13
|
+
const s = Math.floor(ms / 1000);
|
|
14
|
+
if (s < 60) return `${s}s`;
|
|
15
|
+
const m = Math.floor(s / 60);
|
|
16
|
+
if (m < 60) return `${m}m`;
|
|
17
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const when = (n) => n.endedAt ?? n.updatedAt ?? n.startedAt ?? 0;
|
|
21
|
+
|
|
22
|
+
/** One agent as a row: who, what it is on, and the one number worth the room. */
|
|
23
|
+
function rowOf(n, now) {
|
|
24
|
+
const state = stateOf(n.status);
|
|
25
|
+
if (state === 'failed') {
|
|
26
|
+
const bits = [];
|
|
27
|
+
if (n.workflow) bits.push(`in ${n.workflow}`);
|
|
28
|
+
// The count the transcript gave. An agent can fail without a tool error; then there is none to report.
|
|
29
|
+
if (n.errors) bits.push(`${n.errors} ${n.errors === 1 ? 'error' : 'errors'}`);
|
|
30
|
+
if (n.status !== 'failed') bits.push(n.status);
|
|
31
|
+
return { id: n.id, node: n, type: typeOf(n), line: bits.join(' · ') || (n.description ?? ''), aside: null };
|
|
32
|
+
}
|
|
33
|
+
const live = state === 'live' || state === 'waking';
|
|
34
|
+
return {
|
|
35
|
+
id: n.id,
|
|
36
|
+
node: n,
|
|
37
|
+
type: typeOf(n),
|
|
38
|
+
line: n.description ?? '',
|
|
39
|
+
// How long it has been going. With no start recorded there is nothing to count from, and nothing is shown.
|
|
40
|
+
aside: state === 'waking' ? 'starting' : live && n.startedAt != null ? short(now - n.startedAt) : null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The sections of the List, top to bottom. A section with nothing in it is left out.
|
|
46
|
+
*
|
|
47
|
+
* @param nodes the graph's nodes
|
|
48
|
+
* @param queue the approval queue (every session started here), as approvals.js orders it
|
|
49
|
+
* @param current the session being looked at
|
|
50
|
+
* @param folded Map sectionKey -> boolean, the viewer's own choices; a section not in it takes its default
|
|
51
|
+
*/
|
|
52
|
+
export function sections(nodes, { queue = [], current = null, folded = new Map(), now = 0 } = {}) {
|
|
53
|
+
const agents = (nodes ?? []).filter((n) => n.kind === 'agent');
|
|
54
|
+
const out = [];
|
|
55
|
+
|
|
56
|
+
// Needs you: every request waiting, this session's first. Each is a card that carries its own answers.
|
|
57
|
+
const cards = [...queue].sort((a, b) => Number(b.sessionId === current) - Number(a.sessionId === current) || (a.askedAt ?? 0) - (b.askedAt ?? 0));
|
|
58
|
+
if (cards.length) out.push({ key: 'needs', title: 'Needs you', count: cards.length, tone: 'waiting', cards, rows: [], folded: false, foldable: false });
|
|
59
|
+
|
|
60
|
+
const by = (pred, sort) => agents.filter(pred).sort(sort).map((n) => rowOf(n, now));
|
|
61
|
+
const newest = (a, b) => when(b) - when(a);
|
|
62
|
+
const oldest = (a, b) => (a.startedAt ?? Infinity) - (b.startedAt ?? Infinity) || String(a.id).localeCompare(String(b.id));
|
|
63
|
+
|
|
64
|
+
const fixed = [
|
|
65
|
+
{ key: 'failed', title: 'Failed', tone: 'fail', rows: by((n) => stateOf(n.status) === 'failed', newest), foldedByDefault: false },
|
|
66
|
+
{ key: 'running', title: 'Running', tone: 'busy', rows: by((n) => ['live', 'waking'].includes(stateOf(n.status)), oldest), foldedByDefault: false },
|
|
67
|
+
{ key: 'done', title: 'Done', tone: 'good', rows: by((n) => stateOf(n.status) === 'done', newest), foldedByDefault: true },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// Everything else, by the status the transcript wrote. `ended` and `stale` have words here; anything the panel
|
|
71
|
+
// does not recognise keeps its own.
|
|
72
|
+
const rest = new Map();
|
|
73
|
+
for (const n of agents) {
|
|
74
|
+
const s = stateOf(n.status);
|
|
75
|
+
if (s === 'failed' || s === 'live' || s === 'waking' || s === 'done') continue;
|
|
76
|
+
const key = String(n.status ?? 'unknown');
|
|
77
|
+
if (!rest.has(key)) rest.set(key, []);
|
|
78
|
+
rest.get(key).push(n);
|
|
79
|
+
}
|
|
80
|
+
const WORD = { ended: 'Ended', stale: 'Stale' };
|
|
81
|
+
for (const [status, members] of [...rest].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
82
|
+
fixed.push({
|
|
83
|
+
key: `status:${status}`, title: WORD[status] ?? status, tone: 'none', known: status in WORD,
|
|
84
|
+
rows: members.sort(newest).map((n) => rowOf(n, now)), foldedByDefault: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const s of fixed) {
|
|
89
|
+
if (!s.rows.length) continue;
|
|
90
|
+
out.push({
|
|
91
|
+
key: s.key, title: s.title, count: s.rows.length, tone: s.tone, cards: [], rows: s.rows,
|
|
92
|
+
folded: folded.has(s.key) ? folded.get(s.key) : s.foldedByDefault, foldable: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** What a request card says under the agent's name. */
|
|
99
|
+
export function wants(item) {
|
|
100
|
+
return `wants to run ${item.toolName}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Which view a window opens on when the viewer has not chosen one: the List on a phone, the graph elsewhere. */
|
|
104
|
+
export function defaultView(stored, phone) {
|
|
105
|
+
if (stored === 'graph' || stored === 'timeline' || stored === 'list') return stored;
|
|
106
|
+
return phone ? 'list' : 'graph';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The graph and the Timeline are drawn for a wide window. On a phone they still open, and say so. */
|
|
110
|
+
export function narrowWarning(view, phone) {
|
|
111
|
+
return phone && view !== 'list' ? 'Best on a wider screen' : null;
|
|
112
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The List view: one column, in the order of what needs the reader.
|
|
2
|
+
//
|
|
3
|
+
// What goes where is decided in list-plan.js. This file draws it. A waiting request is a card that carries its
|
|
4
|
+
// own three answers, so in this view the approval dock is not needed and is not shown.
|
|
5
|
+
import { sections, wants } from './list-plan.js';
|
|
6
|
+
import { Press } from './press.js';
|
|
7
|
+
import { remaining, asker, allowSessionLabel, VERDICTS } from './approvals.js';
|
|
8
|
+
|
|
9
|
+
function mk(tag, cls, text) {
|
|
10
|
+
const n = document.createElement(tag);
|
|
11
|
+
if (cls) n.className = cls;
|
|
12
|
+
if (text != null) n.textContent = text;
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class List {
|
|
17
|
+
/**
|
|
18
|
+
* @param hooks.tileOf(node) the agent's mark
|
|
19
|
+
* @param hooks.onSelect(node) a row was chosen
|
|
20
|
+
* @param hooks.onDecide(item, verdict)
|
|
21
|
+
* @param hooks.onLocate(item) a request in another session: go to it
|
|
22
|
+
* @param hooks.dimmed(node) is this agent outside what Show is showing
|
|
23
|
+
*/
|
|
24
|
+
constructor(root, hooks = {}) {
|
|
25
|
+
this.root = root;
|
|
26
|
+
this.hooks = hooks;
|
|
27
|
+
// A redraw waits while a button is down on the view: the element under a press has to live to the release.
|
|
28
|
+
this.press = new Press(() => this.render());
|
|
29
|
+
if (typeof window !== 'undefined' && root.addEventListener) this.press.watch(root, window);
|
|
30
|
+
this.nodes = [];
|
|
31
|
+
this.queue = [];
|
|
32
|
+
this.current = null;
|
|
33
|
+
this.folded = new Map();
|
|
34
|
+
this.selected = null;
|
|
35
|
+
this.busy = new Set();
|
|
36
|
+
this.now = () => Date.now();
|
|
37
|
+
this.root.classList.add('ls');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setSession(id) { if (id !== this.current) { this.current = id; this.nodes = []; this.folded = new Map(); this.selected = null; this.render(); } }
|
|
41
|
+
setNodes(nodes) { this.nodes = nodes ?? []; this.render(); }
|
|
42
|
+
|
|
43
|
+
setQueue(queue) {
|
|
44
|
+
this.queue = queue ?? [];
|
|
45
|
+
for (const key of [...this.busy]) if (!this.queue.some((r) => r.key === key)) this.busy.delete(key);
|
|
46
|
+
this.render();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
select(id) { this.selected = id; this.render(); }
|
|
50
|
+
|
|
51
|
+
/** An answer did not reach the server: the buttons come back. */
|
|
52
|
+
release(key) { this.busy.delete(key); this.render(); }
|
|
53
|
+
|
|
54
|
+
/** Once a second: the countdowns and the running times move. */
|
|
55
|
+
tick() { if (!this.root.hidden && (this.queue.length || this.nodes.some((n) => n.status === 'running'))) this.render(); }
|
|
56
|
+
|
|
57
|
+
render() {
|
|
58
|
+
if (this.root.hidden) return;
|
|
59
|
+
if (this.press.defer()) return;
|
|
60
|
+
const now = this.now();
|
|
61
|
+
const list = sections(this.nodes, { queue: this.queue, current: this.current, folded: this.folded, now });
|
|
62
|
+
if (!list.length) {
|
|
63
|
+
const empty = mk('div', 'ls-empty');
|
|
64
|
+
empty.append(mk('strong', null, 'No agents yet'),
|
|
65
|
+
mk('span', null, 'This session has not delegated to a subagent. When it does, each one is listed here.'));
|
|
66
|
+
this.root.replaceChildren(empty);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const focused = this.root.contains?.(document.activeElement) ? document.activeElement.dataset?.key ?? null : null;
|
|
70
|
+
const top = this.root.scrollTop;
|
|
71
|
+
this.root.replaceChildren(...list.map((s) => this.#section(s, now)));
|
|
72
|
+
this.root.scrollTop = top;
|
|
73
|
+
if (focused) [...this.root.querySelectorAll('[data-key]')].find((el) => el.dataset.key === focused)?.focus();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#section(s, now) {
|
|
77
|
+
const sec = mk('section', 'ls-sec');
|
|
78
|
+
sec.dataset.tone = s.tone;
|
|
79
|
+
const head = mk(s.foldable ? 'button' : 'div', 'ls-head');
|
|
80
|
+
const dot = mk('span', 'dot');
|
|
81
|
+
dot.dataset.tone = s.tone;
|
|
82
|
+
head.append(dot, mk('span', 'ls-title', `${s.title} · ${s.count}`));
|
|
83
|
+
if (s.foldable) {
|
|
84
|
+
head.type = 'button';
|
|
85
|
+
head.dataset.key = `sec:${s.key}`;
|
|
86
|
+
head.setAttribute('aria-expanded', String(!s.folded));
|
|
87
|
+
head.append(mk('span', 'row-fill'), mk('span', 'sub', s.folded ? 'show' : 'hide'));
|
|
88
|
+
head.addEventListener('click', () => { this.folded.set(s.key, !s.folded); this.render(); });
|
|
89
|
+
}
|
|
90
|
+
sec.append(head);
|
|
91
|
+
for (const item of s.cards) sec.append(this.#card(item, now));
|
|
92
|
+
if (!s.folded) for (const r of s.rows) sec.append(this.#row(r));
|
|
93
|
+
return sec;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A waiting request: who wants what, how long is left, and the three answers. */
|
|
97
|
+
#card(item, now) {
|
|
98
|
+
const card = mk('div', 'ls-card');
|
|
99
|
+
const head = mk('div', 'ls-card-head');
|
|
100
|
+
const who = mk('div', 'ls-card-who');
|
|
101
|
+
who.append(mk('span', 'ls-name', asker(item)), mk('span', 'sub', wants(item)));
|
|
102
|
+
const r = remaining(item, now);
|
|
103
|
+
const clock = mk('span', 'ls-clock', r.known ? `${r.left}s` : '?');
|
|
104
|
+
clock.setAttribute('role', 'timer');
|
|
105
|
+
clock.setAttribute('aria-label', r.known ? `Auto-deny in ${r.left} seconds` : 'Time left not measured');
|
|
106
|
+
head.append(who, mk('span', 'row-fill'), clock);
|
|
107
|
+
card.append(head);
|
|
108
|
+
if (item.detail) card.append(mk('pre', 'ls-cmd', item.detail));
|
|
109
|
+
// A request from another session says which, and goes there.
|
|
110
|
+
if (item.sessionId !== this.current) {
|
|
111
|
+
const where = mk('button', 'ls-where', `in ${item.sessionName}`);
|
|
112
|
+
where.type = 'button';
|
|
113
|
+
where.addEventListener('click', () => this.hooks.onLocate?.(item));
|
|
114
|
+
card.append(where);
|
|
115
|
+
}
|
|
116
|
+
const sent = this.busy.has(item.key);
|
|
117
|
+
const acts = mk('div', 'ls-acts');
|
|
118
|
+
for (const [verdict, label, cls] of [
|
|
119
|
+
['allow', VERDICTS.allow.label, 'btn primary'],
|
|
120
|
+
['always', allowSessionLabel(item.toolName), 'btn'],
|
|
121
|
+
['deny', VERDICTS.deny.label, 'btn'],
|
|
122
|
+
]) {
|
|
123
|
+
const b = mk('button', cls, label);
|
|
124
|
+
b.type = 'button';
|
|
125
|
+
b.disabled = sent;
|
|
126
|
+
b.dataset.key = `card:${item.key}:${verdict}`;
|
|
127
|
+
b.addEventListener('click', () => {
|
|
128
|
+
if (this.busy.has(item.key)) return;
|
|
129
|
+
this.busy.add(item.key);
|
|
130
|
+
this.render();
|
|
131
|
+
this.hooks.onDecide?.(item, verdict);
|
|
132
|
+
});
|
|
133
|
+
acts.append(b);
|
|
134
|
+
}
|
|
135
|
+
card.append(acts);
|
|
136
|
+
return card;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#row(r) {
|
|
140
|
+
const row = mk('button', 'ls-row');
|
|
141
|
+
row.type = 'button';
|
|
142
|
+
row.dataset.key = `row:${r.id}`;
|
|
143
|
+
if (r.id === this.selected) row.classList.add('on');
|
|
144
|
+
if (this.hooks.dimmed?.(r.node)) row.classList.add('dim');
|
|
145
|
+
const tile = this.hooks.tileOf?.(r.node);
|
|
146
|
+
if (tile) row.append(tile);
|
|
147
|
+
const text = mk('span', 'ls-text');
|
|
148
|
+
text.append(mk('span', 'ls-name', r.type), mk('span', 'ls-line', r.line));
|
|
149
|
+
row.append(text);
|
|
150
|
+
if (r.aside) row.append(mk('span', 'sub ls-aside', r.aside));
|
|
151
|
+
const chev = mk('span', 'ls-chev');
|
|
152
|
+
chev.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg>';
|
|
153
|
+
row.append(chev);
|
|
154
|
+
row.addEventListener('click', () => this.hooks.onSelect?.(r.node));
|
|
155
|
+
return row;
|
|
156
|
+
}
|
|
157
|
+
}
|