@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-fork.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 +67 -0
- package/dist/src/agent-execution.js +36 -51
- package/dist/src/background-widget.d.ts +56 -0
- package/dist/src/background-widget.js +22 -15
- package/dist/src/decoders.js +2 -1
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-view.d.ts +16 -1
- package/dist/src/host-view.js +109 -42
- package/dist/src/host.js +57 -24
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +867 -545
- package/dist/src/index.js.map +3 -3
- package/dist/src/registry.js +2 -1
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +325 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +253 -0
- package/dist/src/trajectory.d.ts +29 -4
- package/dist/src/trajectory.js +58 -3
- package/dist/src/types.d.ts +0 -1
- package/dist/src/utils.d.ts +2 -1
- package/dist/src/utils.js +10 -0
- package/dist/src/validation.d.ts +5 -17
- package/dist/src/validation.js +9 -458
- package/dist/subagents/index.js +1132 -694
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -0
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +2 -1
- package/dist/subagents/src/manager.js +29 -10
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +23 -2
- package/dist/subagents/src/view.js +96 -30
- package/dist/trajectory/assets/index.html +347 -231
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +377 -257
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +347 -231
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +143 -42
- package/dist/trajectory/src/server.js +134 -9
- package/dist/trajectory/src/server.js.map +1 -1
- package/package.json +2 -1
- package/src/agent-execution.ts +36 -42
- package/src/background-widget.ts +25 -18
- package/src/decoders.ts +2 -1
- package/src/host-navigator.ts +58 -14
- package/src/host-view.ts +93 -37
- package/src/host.ts +51 -21
- package/src/index.ts +3 -1
- package/src/registry.ts +2 -1
- package/src/roles.ts +326 -0
- package/src/settings.ts +206 -0
- package/src/trajectory.ts +76 -7
- package/src/types.ts +1 -1
- package/src/utils.ts +8 -1
- package/src/validation.ts +12 -356
- package/subagents/README.md +1 -1
- package/subagents/src/contracts.ts +4 -0
- package/subagents/src/index.ts +3 -2
- package/subagents/src/manager.ts +28 -10
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +97 -28
- package/trajectory/src/assets/index.html +347 -231
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +118 -38
- package/trajectory/src/server.ts +112 -12
|
@@ -5,112 +5,103 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Trajectory</title>
|
|
7
7
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
8
|
+
<script>try { const theme = localStorage.getItem("traj-theme"); if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme; } catch { /* Preferences are optional. */ }</script>
|
|
8
9
|
<style>
|
|
10
|
+
/* Paper sheets on a desk. Colours follow the system scheme unless html[data-theme] pins light or dark. */
|
|
9
11
|
:root {
|
|
10
|
-
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
12
|
+
color-scheme: light dark;
|
|
13
|
+
--serif: "Iowan Old Style", Charter, "Bitstream Charter", "Palatino Linotype", Georgia, serif;
|
|
14
|
+
--font: Inter, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif;
|
|
15
|
+
--bg: light-dark(#ece5d6, #12110f); --bg-2: light-dark(#fbf8f1, #1c1a17); --bg-3: light-dark(#f3eee3, #26231f);
|
|
16
|
+
--bg-hover: light-dark(#f2ecdf, #25221e); --bg-sel: light-dark(#f7e6b5, #3a3222); --line: light-dark(#e4dbc9, #312d27);
|
|
17
|
+
--text: light-dark(#2b2723, #ebe4d6); --dim: light-dark(#6f665a, #a89e8e); --mute: light-dark(#a19682, #6f675b);
|
|
18
|
+
--accent: light-dark(#2f5d8a, #8db4de); --ok: light-dark(#4d7c3a, #9cc47f); --warn: light-dark(#b7791f, #e2b45c); --err: light-dark(#b3402e, #ea8b76);
|
|
19
|
+
--side: var(--bg-2); --insp: var(--bg-2); --left: 300px; --right: 360px; --swim: 220px;
|
|
20
|
+
--rule: light-dark(#ece4d3, #26231f); --margin: light-dark(#e7b9a9, #4a302a);
|
|
21
|
+
--sheet: 0 1px 2px light-dark(#5a47261f, #0000004d), 0 12px 28px -16px light-dark(#5a472640, #00000080);
|
|
22
|
+
--code-bg: light-dark(#f5f0e4, #161412); --code-text: light-dark(#3b352d, #ddd5c6);
|
|
23
|
+
--note-warn: light-dark(#fff5d9, #2d2617); --note-err: light-dark(#fbe8e2, #321f1a);
|
|
24
|
+
--attn-bg: light-dark(#f6e3b4, #3d321c); --attn-text: light-dark(#7a5310, #e9c476);
|
|
25
|
+
--bar-ok: light-dark(#b9cfa6, #3d5534); --bar-spin: light-dark(#a9c1dc, #34506e); --bar-fail: light-dark(#e3a595, #6b3a30);
|
|
26
|
+
--bar-wait: light-dark(#ecd29b, #5d4b25); --bar-paused: light-dark(#d6cfc2, #3a3630); --model: light-dark(#6c56b8, #b39ce8);
|
|
27
|
+
--grain: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 .35 0 0 0 0 .28 0 0 0 0 .18 0 0 0 .07 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
|
15
28
|
}
|
|
16
|
-
html[data-theme="
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
--line: #313244; --text: #cdd6f4; --dim: #a6adc8; --mute: #6c7086; --accent: #89b4fa;
|
|
20
|
-
--ok: #a6e3a1; --warn: #f9e2af; --err: #f38ba8; --side: #181825; --insp: #181825;
|
|
21
|
-
}
|
|
22
|
-
html[data-theme="paper"] {
|
|
23
|
-
--font: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
24
|
-
--bg: #f6f7f8; --bg-2: #eef0f2; --bg-3: #e4e7eb; --bg-hover: #e8eaed; --bg-sel: #dbe7ff;
|
|
25
|
-
--line: #d8dce2; --text: #121417; --dim: #5b6370; --mute: #8b929c; --accent: #2563eb;
|
|
26
|
-
--ok: #17803d; --warn: #b45309; --err: #c41c1c; --side: #eef0f2; --insp: #f3f4f6;
|
|
27
|
-
}
|
|
28
|
-
* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: var(--mute) var(--bg-2); }
|
|
29
|
+
html[data-theme="light"] { color-scheme: light; }
|
|
30
|
+
html[data-theme="dark"] { color-scheme: dark; }
|
|
31
|
+
* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: var(--mute) transparent; }
|
|
29
32
|
*::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
30
|
-
*::-webkit-scrollbar-track { background:
|
|
33
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
31
34
|
*::-webkit-scrollbar-thumb { background: var(--mute); border: 2px solid var(--bg-2); border-radius: 6px; }
|
|
32
35
|
*::-webkit-scrollbar-thumb:hover { background: var(--dim); }
|
|
33
|
-
|
|
34
|
-
html[data-theme="paper"] { color-scheme: light; }
|
|
35
|
-
html, body { height: 100%; margin: 0; background: var(--bg); color: var(--text); font: 13px/1.4 var(--font); }
|
|
36
|
-
html[data-theme="tty"] body { letter-spacing: -0.02em; }
|
|
37
|
-
html[data-theme="tty"] * { border-radius: 0 !important; }
|
|
36
|
+
html, body { height: 100%; margin: 0; background: var(--grain), var(--bg); color: var(--text); font: 13.5px/1.5 var(--font); -webkit-font-smoothing: antialiased; }
|
|
38
37
|
button, input { font: inherit; color: inherit; }
|
|
39
38
|
button { cursor: pointer; }
|
|
40
|
-
.app { height: 100%; display: flex; min-width: 0; }
|
|
41
|
-
.sidebar
|
|
42
|
-
.
|
|
43
|
-
.splitbar
|
|
44
|
-
.splitbar:
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.brand
|
|
39
|
+
.app { height: 100%; display: flex; min-width: 0; padding: 12px; }
|
|
40
|
+
.sidebar, .stage, .inspector { background: var(--bg-2); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--sheet); overflow: hidden; }
|
|
41
|
+
.sidebar { width: var(--left); flex: none; display: flex; flex-direction: column; padding: 16px 12px; overflow: auto; min-width: 180px; }
|
|
42
|
+
.splitbar { width: 12px; flex: none; cursor: col-resize; background: transparent; }
|
|
43
|
+
.splitbar.row { width: auto; height: 10px; padding: 0; cursor: row-resize; }
|
|
44
|
+
.splitbar:hover, .splitbar.drag { background: linear-gradient(var(--accent), var(--accent)) center / 2px 40% no-repeat; }
|
|
45
|
+
.splitbar.row:hover, .splitbar.row.drag { background: linear-gradient(var(--accent), var(--accent)) center / 40% 2px no-repeat; }
|
|
46
|
+
.brand { padding: 2px 4px 16px 8px; display: flex; align-items: center; gap: 6px; }
|
|
47
|
+
.brand-name { flex: 1; font: italic 600 21px/1 var(--serif); color: var(--text); }
|
|
48
|
+
.theme-toggle { width: 26px; height: 26px; flex: none; border: 1px solid var(--line); border-radius: 50%; background: var(--bg-3); color: var(--dim); font-size: 13px; line-height: 1; padding: 0; }
|
|
49
|
+
.theme-toggle:hover { color: var(--text); border-color: var(--mute); }
|
|
48
50
|
.group { margin-bottom: 6px; }
|
|
49
|
-
.folder { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left; border: 0; background: none; color: var(--text); padding: 4px 6px; border-radius:
|
|
51
|
+
.folder { display: flex; align-items: center; gap: 6px; width: 100%; text-align: left; border: 0; background: none; color: var(--text); padding: 4px 6px; border-radius: 8px; }
|
|
50
52
|
.folder:hover { background: var(--bg-hover); }
|
|
51
53
|
.folder .marker { width: 10px; color: var(--dim); }
|
|
52
54
|
.folder .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
|
53
55
|
.folder .count { color: var(--dim); font: 11px ui-monospace, Menlo, Consolas, monospace; }
|
|
54
56
|
.group-children { margin-left: 8px; }
|
|
55
|
-
.pub { border-radius:
|
|
56
|
-
.pub.on { background: #
|
|
57
|
+
.pub { border-radius: 10px; padding: 4px 6px; }
|
|
58
|
+
.pub.on { background: light-dark(#f4eee1, #24211d); }
|
|
57
59
|
.pub .row { display: flex; align-items: center; gap: 8px; }
|
|
58
60
|
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--mute); }
|
|
59
|
-
.dot.live { background: var(--ok); box-shadow: 0 0 0 3px
|
|
61
|
+
.dot.live { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 18%, transparent); }
|
|
60
62
|
.pub .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
|
61
|
-
.attn { margin-left: auto; font-size: 10px; color: var(--
|
|
62
|
-
.children { margin: 0 0 0 8px; padding: 2px 0 0 8px; border-left: 1px solid
|
|
63
|
-
.run-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: var(--dim); padding: 4px 6px; border-radius:
|
|
64
|
-
.run-item
|
|
63
|
+
.attn { margin-left: auto; font-size: 10px; font-weight: 600; background: var(--attn-bg); color: var(--attn-text); border-radius: 999px; padding: 1px 7px; }
|
|
64
|
+
.children { margin: 0 0 0 8px; padding: 2px 0 0 8px; border-left: 1px solid var(--line); }
|
|
65
|
+
.run-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: var(--dim); padding: 4px 6px; border-radius: 8px; font-size: 12px; }
|
|
66
|
+
.run-item:hover { background: var(--bg-hover); }
|
|
67
|
+
.run-item.on { background: var(--bg-sel); color: var(--text); box-shadow: inset 3px 0 0 var(--warn); }
|
|
68
|
+
.more-items.section-toggle { margin: 2px 0; padding: 4px 6px; color: var(--mute); font-size: 11px; border-radius: 8px; }
|
|
69
|
+
.more-items.section-toggle:hover { color: var(--text); background: var(--bg-hover); }
|
|
65
70
|
.subagents-head { margin: 4px 0 2px; padding: 3px 6px; font-size: 10px; letter-spacing: .08em; color: var(--mute); }
|
|
66
|
-
.steer-input { min-width: 180px; flex: 1; background: var(--bg-3); border: 1px solid var(--line); border-radius:
|
|
71
|
+
.steer-input { min-width: 180px; flex: 1; background: var(--bg-3); border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px; }
|
|
67
72
|
.g-run { width: 14px; flex: none; display: inline-block; text-align: center; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 13px; line-height: 1; }
|
|
68
73
|
.g-run.ok { color: var(--ok); }
|
|
69
74
|
.g-run.wait { color: var(--warn); }
|
|
70
75
|
.g-run.fail { color: var(--err); }
|
|
71
76
|
.g-run.queued, .g-run.paused { color: var(--mute); }
|
|
72
77
|
.g-run.spin { color: var(--accent); }
|
|
73
|
-
|
|
78
|
+
/* Rotating a transform only repaints the glyph; animating `content` relaid out the whole document each step. */
|
|
79
|
+
.g-run.spin::before { content: "◜"; display: inline-block; animation: spin-glyph .8s linear infinite; will-change: transform; }
|
|
74
80
|
.g-run.budget { color: var(--warn); }
|
|
75
|
-
@keyframes
|
|
76
|
-
0% { content: "⠋"; } 12% { content: "⠙"; } 25% { content: "⠹"; } 37% { content: "⠸"; }
|
|
77
|
-
50% { content: "⠼"; } 62% { content: "⠴"; } 75% { content: "⠦"; } 87% { content: "⠧"; } 100% { content: "⠇"; }
|
|
78
|
-
}
|
|
81
|
+
@keyframes spin-glyph { to { transform: rotate(360deg); } }
|
|
79
82
|
.run-item .n { min-width: 0; flex: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
80
|
-
.run-item .await { flex: none; font-size: 10px; font-weight: 600; padding: 1px
|
|
83
|
+
.run-item .await { flex: none; font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 999px; background: var(--attn-bg); color: var(--attn-text); }
|
|
81
84
|
.run-item .tel { flex: none; margin-left: auto; font-size: 10px; color: var(--mute); font-family: ui-monospace, Menlo, Consolas, monospace; white-space: nowrap; }
|
|
82
85
|
.stage { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
83
|
-
.top { display: flex; align-items: center; gap: 14px; padding:
|
|
84
|
-
.crumb {
|
|
86
|
+
.top { display: flex; align-items: center; gap: 14px; padding: 14px 20px 12px; border-bottom: 1px solid var(--line); }
|
|
87
|
+
.crumb { font: 16px/1.2 var(--serif); color: var(--dim); }
|
|
88
|
+
.crumb #canvas-run, .crumb #agent-crumb { color: var(--text); font-weight: 600; }
|
|
85
89
|
.crumb a { color: var(--accent); text-decoration: none; cursor: pointer; }
|
|
86
90
|
.run-stats { margin-left: auto; color: var(--dim); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; display: flex; align-items: center; gap: 8px; }
|
|
87
91
|
.hidden { display: none !important; }
|
|
88
|
-
.toolbar { display: flex; align-items: center; gap: 6px; padding:
|
|
89
|
-
.seg button { background: none; border: 0; color: var(--dim); padding: 4px 8px; border-radius:
|
|
92
|
+
.toolbar { display: flex; align-items: center; gap: 6px; padding: 10px 20px; border-bottom: 1px solid var(--line); }
|
|
93
|
+
.seg button { background: none; border: 0; color: var(--dim); padding: 4px 8px; border-radius: 6px; }
|
|
90
94
|
.seg button.on { background: var(--bg-3); color: var(--text); }
|
|
91
|
-
.search { margin-left: auto; background: var(--bg-3); border: 1px solid var(--line); border-radius:
|
|
95
|
+
.search { margin-left: auto; background: var(--bg-3); border: 1px solid var(--line); border-radius: 999px; padding: 6px 12px; width: 180px; }
|
|
92
96
|
.body { flex: 1; min-height: 0; display: flex; }
|
|
93
|
-
.inspector { width: var(--right); flex: none;
|
|
97
|
+
.inspector { width: var(--right); flex: none; min-width: 220px; display: flex; flex-direction: column; }
|
|
94
98
|
.inspector .ins-head, .inspector .itabs { flex: none; }
|
|
95
99
|
.inspector .ins-body { flex: 1; min-height: 0; overflow: auto; }
|
|
96
100
|
#tool-pane, #sys-pane { overflow: visible; }
|
|
97
101
|
.inspector .code:not(.script) { flex: 1; max-height: none; min-height: 0; }
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.splitbar { display: none; }
|
|
102
|
-
.splitbar.row { display: block; }
|
|
103
|
-
.stage { min-height: 48vh; }
|
|
104
|
-
.top, .toolbar { flex-wrap: wrap; }
|
|
105
|
-
.run-stats { margin-left: 0; width: 100%; }
|
|
106
|
-
.search { width: 100%; margin: 6px 0 0; }
|
|
107
|
-
.modal { inset: 8px; }
|
|
108
|
-
.swim .lane { grid-template-columns: 16px 56px 1fr; }
|
|
109
|
-
.swim .lane.child { grid-template-columns: 16px 44px 1fr; }
|
|
110
|
-
.swim .axis { grid-template-columns: 80px 1fr; }
|
|
111
|
-
}
|
|
112
|
-
.swim { height: var(--swim); flex: 0 0 var(--swim); min-height: 0; overflow-y: auto; padding: 8px 14px 12px; border-bottom: 1px solid var(--line); background: linear-gradient(#1d1d1d 1px, transparent 1px) 0 0 / 100% 18px, linear-gradient(90deg, #1d1d1d 1px, transparent 1px) 0 0 / 24px 100%, #151515; position: relative; user-select: none; }
|
|
113
|
-
.brush { position: absolute; top: 22px; bottom: 8px; background: #6ea0ff22; border-left: 1px solid #6ea0ff99; border-right: 1px solid #6ea0ff99; pointer-events: none; z-index: 3; }
|
|
102
|
+
.mobile-tabs { display: none; }
|
|
103
|
+
.swim { height: var(--swim); flex: 0 0 var(--swim); min-height: 0; overflow-y: auto; padding: 8px 14px 12px; border-bottom: 1px solid var(--line); background: linear-gradient(90deg, transparent 117px, var(--margin) 117px 118px, transparent 118px), repeating-linear-gradient(transparent 0 23px, var(--rule) 23px 24px), var(--bg-2); position: relative; user-select: none; }
|
|
104
|
+
.brush { position: absolute; top: 22px; bottom: 8px; background: color-mix(in srgb, var(--accent) 14%, transparent); border-left: 1px solid var(--accent); border-right: 1px solid var(--accent); pointer-events: none; z-index: 3; }
|
|
114
105
|
.range-edge { position: absolute; top: -17px; color: var(--accent); background: var(--bg-2); padding: 1px 3px; font: 10px ui-monospace, Menlo, Consolas, monospace; white-space: nowrap; text-shadow: 0 1px var(--bg); z-index: 1; }
|
|
115
106
|
.range-edge.start { left: 3px; }
|
|
116
107
|
.range-edge.end { right: 3px; text-align: right; }
|
|
@@ -120,39 +111,42 @@
|
|
|
120
111
|
.swim .axis { position: sticky; top: 0; z-index: 4; background: var(--bg-2); }
|
|
121
112
|
.axis .ticks { display: flex; justify-content: space-between; position: relative; }
|
|
122
113
|
.timeline .axis { grid-template-columns: 44px 1fr; gap: 8px; margin-bottom: 2px; }
|
|
123
|
-
.now { position: absolute; width: 1px; background:
|
|
114
|
+
.now { position: absolute; width: 1px; background: var(--err); z-index: 2; pointer-events: none; }
|
|
124
115
|
.swim > .now { top: 22px; bottom: 8px; z-index: 5; }
|
|
125
116
|
.timeline > .now { top: 18px; bottom: 2px; }
|
|
126
|
-
.now span { position: absolute; top: -14px; left: 4px; color:
|
|
127
|
-
.lane { display: grid; grid-template-columns: 16px 76px 1fr; gap: 8px; align-items: center; height:
|
|
128
|
-
.lane:hover { background:
|
|
117
|
+
.now span { position: absolute; top: -14px; left: 4px; color: var(--err); background: var(--bg-2); padding: 1px 3px; white-space: nowrap; font: 10px ui-monospace, Menlo, Consolas, monospace; }
|
|
118
|
+
.lane { display: grid; grid-template-columns: 16px 76px 1fr; gap: 8px; align-items: center; height: 20px; margin: 2px 0; cursor: pointer; border-radius: 6px; }
|
|
119
|
+
.lane:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
|
|
129
120
|
.lane.child { padding-left: 12px; grid-template-columns: 16px 64px 1fr; }
|
|
130
121
|
.lane.crumb { cursor: default; opacity: .85; }
|
|
131
122
|
.lane.crumb:hover { background: none; }
|
|
132
|
-
.lane.crumb .lab { text-align: left; color:
|
|
133
|
-
.bar.crumb { background:
|
|
134
|
-
.crumbband { font-size: 11px; color:
|
|
135
|
-
.lane .lab { color: var(--
|
|
136
|
-
.track { position: relative; height:
|
|
137
|
-
.bar { position: absolute; top: 0; height:
|
|
138
|
-
.bar.tool { top:
|
|
139
|
-
.bar.ok { background:
|
|
140
|
-
.bar.spin { background:
|
|
141
|
-
.bar.fail { background: var(--
|
|
142
|
-
.bar.wait { background: var(--
|
|
143
|
-
.bar.paused { background:
|
|
123
|
+
.lane.crumb .lab { text-align: left; color: var(--mute); font-style: italic; }
|
|
124
|
+
.bar.crumb { background: var(--line); height: 4px; top: 3px; }
|
|
125
|
+
.crumbband { font-size: 11px; color: var(--mute); padding: 4px 12px 2px 28px; font-family: ui-monospace, Menlo, Consolas, monospace; font-style: italic; }
|
|
126
|
+
.lane .lab { color: var(--dim); font-size: 11px; text-align: right; overflow: hidden; white-space: nowrap; }
|
|
127
|
+
.track { position: relative; height: 12px; }
|
|
128
|
+
.bar { position: absolute; top: 0; height: 12px; border-radius: 999px; }
|
|
129
|
+
.bar.tool { top: 3px; height: 6px; border-radius: 2px; }
|
|
130
|
+
.bar.ok { background: var(--bar-ok); }
|
|
131
|
+
.bar.spin { background: var(--bar-spin); }
|
|
132
|
+
.bar.fail { background: var(--bar-fail); }
|
|
133
|
+
.bar.wait { background: var(--bar-wait); }
|
|
134
|
+
.bar.paused { background: var(--bar-paused); }
|
|
135
|
+
.bar.tool.ok { background: var(--ok); }
|
|
136
|
+
.bar.tool.fail { background: var(--err); }
|
|
144
137
|
.split { flex: 1; min-height: 0; display: flex; }
|
|
145
138
|
.list { flex: 1; overflow: auto; padding: 8px 0; }
|
|
146
139
|
.workflow-card { border-bottom: 1px solid var(--line); }
|
|
147
|
-
.workflow-head { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; gap: 8px; width: 100%; align-items: center; padding:
|
|
148
|
-
.workflow-head:hover, .workflow-head.on { background: var(--bg-
|
|
149
|
-
.workflow-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font
|
|
140
|
+
.workflow-head { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; gap: 8px; width: 100%; align-items: center; padding: 12px 20px; border: 0; background: none; color: inherit; text-align: left; }
|
|
141
|
+
.workflow-head:hover, .workflow-head.on { background: var(--bg-hover); }
|
|
142
|
+
.workflow-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 600 16px/1.2 var(--serif); }
|
|
150
143
|
.workflow-meta { color: var(--dim); font: 11px ui-monospace, Menlo, Consolas, monospace; white-space: nowrap; }
|
|
151
144
|
.agent-grid { min-width: 900px; padding: 0 12px 8px 28px; }
|
|
152
145
|
.agent-grid-head, .agent-grid-row { display: grid; grid-template-columns: minmax(220px, 2fr) minmax(140px, 1.2fr) 80px minmax(150px, 1.5fr) 80px 72px 64px; gap: 8px; align-items: center; }
|
|
153
146
|
.agent-grid-head { padding: 4px 8px; color: var(--mute); font-size: 10px; letter-spacing: .05em; }
|
|
154
|
-
.agent-grid-row { min-height: 24px; padding: 3px 8px; cursor: pointer; font-size: 11px; }
|
|
155
|
-
.agent-grid-row:hover
|
|
147
|
+
.agent-grid-row { min-height: 24px; padding: 3px 8px; cursor: pointer; font-size: 11px; border-radius: 8px; }
|
|
148
|
+
.agent-grid-row:hover { background: var(--bg-hover); }
|
|
149
|
+
.agent-grid-row.on { background: var(--bg-sel); }
|
|
156
150
|
.agent-grid-row .agent-cell { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
157
151
|
.agent-grid-row .agent-cell:first-child { color: var(--text); font-family: ui-monospace, Menlo, Consolas, monospace; }
|
|
158
152
|
.agent-grid-scope { padding: 6px 8px 2px; color: var(--dim); font: 11px ui-monospace, Menlo, Consolas, monospace; font-style: italic; }
|
|
@@ -170,33 +164,33 @@
|
|
|
170
164
|
.tree { color: var(--dim); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
|
|
171
165
|
.tree .nm { color: var(--text); }
|
|
172
166
|
.note { color: var(--err); font-size: 11px; }
|
|
173
|
-
.phaseband {
|
|
174
|
-
.timeline { padding: 6px 12px 8px; border-bottom: 1px solid var(--line); background: linear-gradient(
|
|
167
|
+
.phaseband { color: var(--accent); padding: 8px 12px 2px; font: italic 13px/1.3 var(--serif); }
|
|
168
|
+
.timeline { padding: 6px 12px 8px; border-bottom: 1px solid var(--line); background: repeating-linear-gradient(transparent 0 15px, var(--rule) 15px 16px), var(--bg-2); position: relative; user-select: none; }
|
|
175
169
|
.timeline > .brush { top: 18px; bottom: 2px; }
|
|
176
170
|
.tl-lane { display: grid; grid-template-columns: 44px 1fr; gap: 8px; align-items: center; height: 16px; }
|
|
177
171
|
.tl-lane .lab { font-size: 10px; color: var(--mute); }
|
|
178
172
|
.ticks { position: relative; height: 12px; }
|
|
179
|
-
.tick { position: absolute; top: 2px; width: 8px; height: 8px; border-radius:
|
|
180
|
-
.tick.in { background:
|
|
181
|
-
.tick.model { background:
|
|
182
|
-
.tick.tl { background:
|
|
173
|
+
.tick { position: absolute; top: 2px; width: 8px; height: 8px; border-radius: 2px; cursor: pointer; }
|
|
174
|
+
.tick.in { background: var(--ok); }
|
|
175
|
+
.tick.model { background: var(--model); }
|
|
176
|
+
.tick.tl { background: var(--warn); }
|
|
183
177
|
.tick.err { background: var(--err); }
|
|
184
178
|
.events { flex: 1; overflow: auto; }
|
|
185
|
-
.evt { display: grid; grid-template-columns: 13.5rem 86px minmax(0, 1fr); gap: 14px; align-items: center; padding: 5px 12px; cursor: pointer; border-left: 2px solid transparent; }
|
|
179
|
+
.evt { display: grid; grid-template-columns: 13.5rem 86px minmax(0, 1fr); gap: 14px; align-items: center; padding: 5px 12px; cursor: pointer; border-left: 2px solid transparent; border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent); }
|
|
186
180
|
.evt > .pill { justify-self: end; }
|
|
187
181
|
.evt:hover { background: var(--bg-hover); }
|
|
188
|
-
.evt.on { background:
|
|
182
|
+
.evt.on { background: var(--bg-sel); border-left-color: var(--warn); }
|
|
189
183
|
@keyframes timeline-event-highlight { from { background: var(--accent); } to { background: transparent; } }
|
|
190
184
|
.evt.timeline-highlight { animation: timeline-event-highlight 1.2s ease-out; }
|
|
191
185
|
.evt .gutter { color: var(--mute); font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
192
|
-
.pill { display: inline-block; width: max-content; min-width: 0; font-size: 10px; font-weight:
|
|
193
|
-
.pill.system { background: #
|
|
194
|
-
.pill.user { background: #
|
|
195
|
-
.pill.context { background: #
|
|
196
|
-
.pill.assistant { background: #
|
|
197
|
-
.pill.tool { background: #
|
|
198
|
-
.pill.skill { background: #
|
|
199
|
-
.pill.issue { background: #
|
|
186
|
+
.pill { display: inline-block; width: max-content; min-width: 0; font-size: 10px; font-weight: 600; letter-spacing: .06em; padding: 2px 7px; border-radius: 999px; text-align: center; }
|
|
187
|
+
.pill.system { background: light-dark(#ebe6db, #2c2925); color: light-dark(#4a443b, #cfc7b8); }
|
|
188
|
+
.pill.user { background: light-dark(#dde7f3, #22324a); color: light-dark(#264a73, #a9c6ea); }
|
|
189
|
+
.pill.context { background: light-dark(#e0ecd6, #23351f); color: light-dark(#3d6a2c, #a9d08f); }
|
|
190
|
+
.pill.assistant { background: light-dark(#ebe2f2, #33273f); color: light-dark(#5a3a78, #d2b9ea); }
|
|
191
|
+
.pill.tool { background: light-dark(#f8e3cc, #3d2a18); color: light-dark(#8a4b12, #f0bf87); }
|
|
192
|
+
.pill.skill { background: light-dark(#d9eeee, #1d3536); color: light-dark(#1f5f63, #9fd8da); }
|
|
193
|
+
.pill.issue { background: light-dark(#f7ddd6, #3f211b); color: light-dark(#9b2f1f, #f1a492); }
|
|
200
194
|
.preview { color: var(--dim); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
|
|
201
195
|
.preview .pill.tool { margin-right: 4px; }
|
|
202
196
|
.preview .preview-tool { font-size: 9px; padding: 1px 4px; }
|
|
@@ -204,47 +198,59 @@
|
|
|
204
198
|
.preview .tool-key, .preview .tool-gap, .preview .tool-timing { color: var(--dim); }
|
|
205
199
|
.preview .tool-value { font-weight: 400; }
|
|
206
200
|
.preview b { color: var(--text); font-weight: 400; }
|
|
207
|
-
.ins-head { padding:
|
|
201
|
+
.ins-head { padding: 16px 16px 10px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--line); font: 600 16px/1.2 var(--serif); }
|
|
202
|
+
.run-stats .agent-details { margin-left: 12px; }
|
|
208
203
|
.ins-sub { color: var(--dim); font-size: 12px; }
|
|
209
204
|
.tool-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
|
|
210
|
-
.itabs { display: flex; gap: 12px; padding: 8px
|
|
205
|
+
.itabs { display: flex; gap: 12px; padding: 8px 16px 0; border-bottom: 1px solid var(--line); }
|
|
211
206
|
.itabs span { color: var(--dim); padding-bottom: 8px; border-bottom: 2px solid transparent; cursor: pointer; font-size: 12px; }
|
|
212
207
|
.itabs span.on { color: var(--accent); border-bottom-color: var(--accent); }
|
|
213
|
-
.ins-body { padding:
|
|
208
|
+
.ins-body { padding: 14px 16px; font-size: 12px; }
|
|
214
209
|
.md { line-height: 1.5; }
|
|
215
210
|
.md h1, .md h2, .md h3 { margin: .7em 0 .3em; font-size: 13px; }
|
|
216
211
|
.md p { margin: 0 0 .7em; }
|
|
217
212
|
.md ul { margin: 0 0 .7em; padding-left: 1.2em; }
|
|
218
|
-
.md code { font-family: ui-monospace, Menlo, Consolas, monospace; background: var(--bg-3); padding: 0 4px; }
|
|
213
|
+
.md code { font-family: ui-monospace, Menlo, Consolas, monospace; background: var(--bg-3); padding: 0 4px; border-radius: 4px; }
|
|
219
214
|
.md a { color: var(--accent); }
|
|
220
|
-
.md pre
|
|
215
|
+
.md pre { background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px; overflow: auto; font: 11px/1.45 ui-monospace, Menlo, Consolas, monospace; white-space: pre-wrap; color: var(--code-text); }
|
|
216
|
+
.md pre code { background: none; padding: 0; }
|
|
221
217
|
.kv { display: grid; grid-template-columns: 90px 1fr; gap: 6px 10px; margin-bottom: 12px; }
|
|
222
218
|
.kv .k { color: var(--dim); }
|
|
223
|
-
.code { background:
|
|
219
|
+
.code { background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; white-space: pre-wrap; color: var(--code-text); overflow: auto; max-height: 240px; }
|
|
224
220
|
.assistant-tools { display: grid; gap: 8px; margin-top: 12px; }
|
|
225
221
|
.assistant-tool { display: grid; gap: 4px; }
|
|
226
|
-
.
|
|
227
|
-
.
|
|
222
|
+
.token.comment, .token.prolog { color: var(--mute); font-style: italic; }
|
|
223
|
+
.token.string, .token.template-string, .token.char, .token.inserted { color: var(--ok); }
|
|
224
|
+
.token.keyword, .token.boolean, .token.builtin, .token.important { color: var(--accent); }
|
|
225
|
+
.token.number, .token.constant, .token.regex { color: var(--warn); }
|
|
226
|
+
.token.property, .token.attr-name, .token.parameter { color: light-dark(#8a3d7a, #d7a1cf); }
|
|
227
|
+
.token.function, .token.class-name { color: light-dark(#2f6f6f, #8fd0cf); }
|
|
228
|
+
.token.operator, .token.punctuation, .token.interpolation-punctuation { color: var(--dim); }
|
|
229
|
+
.token.deleted { color: var(--err); }
|
|
228
230
|
.code.script { max-height: 4.8em; cursor: pointer; }
|
|
229
|
-
.code.script:hover { border-color:
|
|
231
|
+
.code.script:hover { border-color: var(--mute); }
|
|
232
|
+
.inspector .code.args { max-height: min(40vh, 420px); overflow: auto; }
|
|
233
|
+
.workflow-args { margin: 14px 0 12px; }
|
|
234
|
+
.workflow-args summary.sec { margin: 0 0 8px; }
|
|
235
|
+
.workflow-args .code.args { margin: 0; }
|
|
236
|
+
.workflow-args-warning { color: var(--warn); margin-top: 8px; }
|
|
230
237
|
.ln { display: grid; grid-template-columns: 44px 1fr; }
|
|
231
238
|
.ln .no { color: var(--mute); text-align: right; padding-right: 12px; user-select: none; }
|
|
232
|
-
.modal-bg { position: fixed; inset: 0; background: #
|
|
233
|
-
.modal { position: fixed; inset: 32px; background:
|
|
239
|
+
.modal-bg { position: fixed; inset: 0; background: light-dark(#2b27234d, #000000a6); backdrop-filter: blur(3px); z-index: 40; }
|
|
240
|
+
.modal { position: fixed; inset: 32px; background: var(--bg-2); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--sheet); z-index: 41; display: flex; flex-direction: column; overflow: hidden; }
|
|
234
241
|
.modal-bar { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--line); }
|
|
235
242
|
.modal-bar .sp { flex: 1; }
|
|
236
|
-
.modal-bar button { background: var(--bg-3); border:
|
|
243
|
+
.modal-bar button { background: var(--bg-3); border: 1px solid var(--line); border-radius: 8px; padding: 4px 10px; }
|
|
237
244
|
.modal .code { flex: 1; max-height: none; border: 0; border-radius: 0; }
|
|
238
245
|
.tool-item { padding: 7px 8px; border-bottom: 1px solid var(--line); }
|
|
239
246
|
.tool-item b { font-family: ui-monospace, Menlo, Consolas, monospace; }
|
|
240
247
|
.tool-item span { color: var(--dim); display: block; }
|
|
241
248
|
.actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
|
|
242
|
-
.actions button { border:
|
|
249
|
+
.actions button { background: var(--bg-2); color: var(--text); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent); padding: 6px 12px; }
|
|
250
|
+
.actions button:hover { border-color: var(--mute); }
|
|
243
251
|
.actions button:disabled { opacity: .45; cursor: not-allowed; }
|
|
244
|
-
.btn-
|
|
245
|
-
.
|
|
246
|
-
.btn-alt { background: #2a4a7a; color: #cfe0ff; }
|
|
247
|
-
.toast { position: fixed; right: 16px; bottom: 16px; background: #111; border: 1px solid #e6c35c; color: #e6c35c; padding: 8px 12px; border-radius: 6px; display: none; z-index: 20; }
|
|
252
|
+
.btn-alt, .actions .btn-alt { background: var(--bg-2); color: var(--accent); border: 1px solid var(--line); border-radius: 8px; padding: 6px 12px; }
|
|
253
|
+
.toast { position: fixed; right: 16px; bottom: 16px; background: var(--bg-2); border: 1px solid var(--warn); color: var(--text); padding: 8px 12px; border-radius: 10px; box-shadow: var(--sheet); display: none; z-index: 20; }
|
|
248
254
|
.hint { color: var(--mute); font-size: 11px; padding: 0 12px 8px; }
|
|
249
255
|
.acct { width: 100%; border-collapse: collapse; font: 11px ui-monospace, Menlo, Consolas, monospace; }
|
|
250
256
|
.acct th { text-align: left; color: var(--mute); font-weight: 500; padding: 0 6px 6px; }
|
|
@@ -252,59 +258,47 @@
|
|
|
252
258
|
.acct .num { text-align: right; color: var(--dim); }
|
|
253
259
|
.acct tr { cursor: pointer; }
|
|
254
260
|
.acct tr:hover td { background: var(--bg-hover); }
|
|
255
|
-
.pending {
|
|
256
|
-
.pending.issue {
|
|
261
|
+
.pending { background: var(--note-warn); border: 0; border-left: 3px solid var(--warn); border-radius: 4px 10px 10px 4px; padding: 10px 12px; margin-bottom: 10px; box-shadow: 0 1px 2px color-mix(in srgb, var(--text) 8%, transparent); }
|
|
262
|
+
.pending.issue { background: var(--note-err); border-left-color: var(--err); }
|
|
257
263
|
.pending .ttl { font-weight: 600; margin-bottom: 4px; display: flex; gap: 6px; align-items: center; }
|
|
258
|
-
.sec { color: var(--mute); font-size:
|
|
264
|
+
.sec { color: var(--mute); font-size: 10.5px; font-weight: 600; letter-spacing: .14em; margin: 14px 0 8px; }
|
|
259
265
|
.section-toggle { display: block; width: 100%; padding: 0; border: 0; background: none; font: inherit; text-align: left; cursor: pointer; }
|
|
260
|
-
#toggle-gantt { padding:
|
|
266
|
+
#toggle-gantt { padding: 14px 20px 6px; }
|
|
261
267
|
.offline { color: var(--warn); font-size: 11px; margin-top: 8px; }
|
|
262
|
-
.share-url { display: flex; gap: 6px; margin-top: 8px; } .share-url input { flex: 1; min-width: 0; background: var(--bg-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
html[data-theme="tty"] .btn-no { background: #313244; color: #f38ba8; }
|
|
297
|
-
html[data-theme="tty"] .btn-alt { background: #313244; color: #89b4fa; }
|
|
298
|
-
html[data-theme="tty"] .code { background: #11111b; }
|
|
299
|
-
html[data-theme="tty"] .code .kw { color: #cba6f7; }
|
|
300
|
-
html[data-theme="tty"] .code .str { color: #a6e3a1; }
|
|
301
|
-
html[data-theme="tty"] .bar.ok { background: #a6e3a1; }
|
|
302
|
-
html[data-theme="tty"] .bar.spin { background: #89b4fa; }
|
|
303
|
-
html[data-theme="tty"] .bar.fail { background: #f38ba8; }
|
|
304
|
-
html[data-theme="tty"] .bar.wait { background: #f9e2af; }
|
|
305
|
-
html[data-theme="tty"] .swim, html[data-theme="tty"] .timeline {
|
|
306
|
-
background: linear-gradient(#313244 1px, transparent 1px) 0 0 / 100% 18px,
|
|
307
|
-
linear-gradient(90deg, #313244 1px, transparent 1px) 0 0 / 24px 100%, #1e1e2e;
|
|
268
|
+
.share-url { display: flex; gap: 6px; margin-top: 8px; } .share-url input { flex: 1; min-width: 0; background: var(--bg-3); color: var(--text); border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px; font: inherit; font-size: 11px; }
|
|
269
|
+
@media (max-width: 900px) {
|
|
270
|
+
.app { flex-direction: column; overflow: hidden; height: 100dvh; padding: 8px 8px 76px; }
|
|
271
|
+
.sidebar, .inspector, .stage { flex: 1; width: 100% !important; min-width: 0; min-height: 0; overflow: auto; }
|
|
272
|
+
html:not([data-mobile-pane="runs"]) .sidebar, html:not([data-mobile-pane="insp"]) .inspector, html[data-mobile-pane]:not([data-mobile-pane="stage"]) .stage { display: none; }
|
|
273
|
+
.splitbar, .splitbar.row { display: none; }
|
|
274
|
+
.mobile-tabs { display: grid; grid-template-columns: repeat(3, 1fr); position: fixed; left: 8px; right: 8px; bottom: 8px; z-index: 30; padding: 6px; gap: 6px; background: var(--bg-2); border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--sheet); }
|
|
275
|
+
.mobile-tabs button { border: 0; background: none; color: var(--dim); padding: 8px 4px; border-radius: 12px; font-weight: 600; font-size: 12px; line-height: 1.1; display: grid; gap: 3px; justify-items: center; }
|
|
276
|
+
.mobile-tabs button b { font: 18px/1 var(--serif); }
|
|
277
|
+
.mobile-tabs button.on { background: var(--bg-sel); color: var(--text); }
|
|
278
|
+
.top, .toolbar { flex-wrap: wrap; }
|
|
279
|
+
.run-stats { margin-left: 0; width: 100%; flex-wrap: wrap; }
|
|
280
|
+
.search { width: 100%; margin: 6px 0 0; }
|
|
281
|
+
.search, .steer-input { font-size: 16px; }
|
|
282
|
+
.modal { inset: 8px; }
|
|
283
|
+
.swim { height: auto; flex: none; max-height: 44vh; background: repeating-linear-gradient(transparent 0 23px, var(--rule) 23px 24px), var(--bg-2); }
|
|
284
|
+
.swim .lane { grid-template-columns: 16px 56px 1fr; }
|
|
285
|
+
.swim .lane.child { grid-template-columns: 16px 44px 1fr; }
|
|
286
|
+
.swim .axis { grid-template-columns: 80px 1fr; }
|
|
287
|
+
.run-item { padding: 9px 8px; }
|
|
288
|
+
.agent-grid { min-width: 0; padding: 0 8px 8px; }
|
|
289
|
+
.agent-grid-head { display: none; }
|
|
290
|
+
.agent-grid-row { grid-template-columns: minmax(0, 1fr) auto auto auto; min-height: 40px; }
|
|
291
|
+
.agent-grid-row > :nth-child(2), .agent-grid-row > :nth-child(4), .agent-grid-row > :nth-child(6) { display: none; }
|
|
292
|
+
.agent-grid-row > :nth-child(3) { color: var(--dim); }
|
|
293
|
+
.evt { grid-template-columns: auto minmax(0, 1fr); gap: 4px 10px; padding: 10px 14px; }
|
|
294
|
+
.evt > .pill { grid-row: 1; grid-column: 1; justify-self: start; }
|
|
295
|
+
.evt .gutter { grid-row: 1; grid-column: 2; justify-self: end; }
|
|
296
|
+
.evt .preview { grid-column: 1 / -1; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
297
|
+
.log-row { grid-template-columns: 5.5rem minmax(0, 1fr); }
|
|
298
|
+
.logs { padding-left: 8px; }
|
|
299
|
+
.itabs { overflow-x: auto; scrollbar-width: none; gap: 16px; }
|
|
300
|
+
.itabs span { flex: none; white-space: nowrap; }
|
|
301
|
+
.ins-head { flex-wrap: wrap; row-gap: 4px; }
|
|
308
302
|
}
|
|
309
303
|
</style>
|
|
310
304
|
</head>
|
|
@@ -336,6 +330,7 @@
|
|
|
336
330
|
<div class="splitbar" id="split-right"></div>
|
|
337
331
|
<aside class="inspector" id="insp"></aside>
|
|
338
332
|
</div>
|
|
333
|
+
<nav class="mobile-tabs" id="mobile-tabs"><button type="button" data-mobile-pane="runs"><b>☰</b>Runs</button><button type="button" data-mobile-pane="stage" class="on"><b>∾</b>Trace</button><button type="button" data-mobile-pane="insp"><b>✎</b>Details</button></nav>
|
|
339
334
|
<div class="toast" id="toast"></div>
|
|
340
335
|
<div class="modal-bg hidden" id="script-bg"></div>
|
|
341
336
|
<div class="modal hidden" id="script-modal">
|
|
@@ -344,6 +339,7 @@
|
|
|
344
339
|
</div>
|
|
345
340
|
<script src="./marked.min.js"></script>
|
|
346
341
|
<script src="./morphdom.min.js"></script>
|
|
342
|
+
<script src="./prism.min.js"></script>
|
|
347
343
|
<script>
|
|
348
344
|
const defaultRunLayout = () => ({ swimHeight: 220, ganttCollapsed: false, agentsCollapsed: false, logsCollapsed: false });
|
|
349
345
|
const maxSwimHeight = () => Math.max(72, window.innerHeight * 0.7);
|
|
@@ -376,7 +372,7 @@
|
|
|
376
372
|
}
|
|
377
373
|
}
|
|
378
374
|
function saveSidebarCollapsed() { try { localStorage.setItem("traj-sidebar-collapsed", JSON.stringify([...state.sidebarCollapsed])); } catch { /* Preferences are optional. */ } }
|
|
379
|
-
const state = { publishers: [], currentPub: null, currentRun: null, currentTarget: null, currentAgent: null, pendingTarget: null, agentRange: null, agentRangeAgent: null, selectedEvent: null, timelineHighlight: null, eventsSig: null, toolPane: "summary", sysPane: "prompt", inspSig: null, socket: null, transcripts: {}, transcriptPending: {}, transcriptSources: {}, transcriptRefresh: {}, transcriptRevision: {}, transcriptStatus: {}, transcriptCacheOrder: [], actionTargets: new Map(), shareUrls: {}, runLayout: loadRunLayout(), sidebarCollapsed: loadSidebarCollapsed() };
|
|
375
|
+
const state = { publishers: [], currentPub: null, currentRun: null, currentTarget: null, currentAgent: null, pendingTarget: null, agentRange: null, agentRangeAgent: null, selectedEvent: null, timelineHighlight: null, eventsSig: null, toolPane: "summary", sysPane: "prompt", inspMode: "event", inspSig: null, socket: null, transcripts: {}, transcriptPending: {}, transcriptSources: {}, transcriptRefresh: {}, transcriptRevision: {}, transcriptStatus: {}, transcriptCacheOrder: [], actionTargets: new Map(), shareUrls: {}, runLayout: loadRunLayout(), sidebarCollapsed: loadSidebarCollapsed(), workflowArgumentsOpen: new Set() };
|
|
380
376
|
let hasAcceptedState = false;
|
|
381
377
|
let socketGeneration = 0;
|
|
382
378
|
let socketReconnect;
|
|
@@ -399,7 +395,8 @@
|
|
|
399
395
|
const transcriptKey = (publisherId, targetId, agentId) => agentId === undefined ? `${publisherId}\tsubagent\t${targetId}` : `${publisherId}\t${targetId}\t${agentId}`;
|
|
400
396
|
const sameValue = (left, right) => { try { return JSON.stringify(left) === JSON.stringify(right); } catch { return false; } };
|
|
401
397
|
function shareArray(previous, incoming, key) { if (!Array.isArray(incoming)) return incoming; const old = Array.isArray(previous) ? previous : []; const byId = new Map(old.map((item) => [item?.[key], item])); const shared = incoming.map((item) => { const prior = byId.get(item?.[key]); return prior !== undefined && sameValue(prior, item) ? prior : item; }); return old.length === shared.length && shared.every((item, index) => item === old[index]) ? old : shared; }
|
|
402
|
-
|
|
398
|
+
// The server omits timing it already delivered at this revision, so the cached entries are carried forward.
|
|
399
|
+
function shareTranscriptMap(previous, incoming) { if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) return incoming; const old = previous && typeof previous === "object" && !Array.isArray(previous) ? previous : {}; const shared = { ...incoming }; for (const [key, entries] of Object.entries(incoming)) { if (Array.isArray(old[key]) && Array.isArray(entries) && sameValue(old[key], entries)) { shared[key] = old[key]; continue; } if (!entries || typeof entries !== "object" || Array.isArray(entries) || entries.timing !== undefined) continue; const prior = old[key]; if (!prior || typeof prior !== "object" || Array.isArray(prior)) { shared[key] = { ...entries, timing: [] }; continue; } shared[key] = prior.revision === entries.revision && prior.status === entries.status ? prior : { ...entries, timing: prior.timing || [] }; } const keys = Object.keys(shared); return keys.length === Object.keys(old).length && keys.every((key) => shared[key] === old[key]) ? old : shared; }
|
|
403
400
|
function shareRunRecord(previous, incoming) { const run = incoming?.run; const oldRun = previous?.run; const agents = shareArray(oldRun?.agents, run?.agents, "id"); const sharedRun = run && typeof run === "object" ? { ...run, agents } : run; const transcripts = shareTranscriptMap(previous?.transcripts, incoming?.transcripts); const shared = { ...incoming, run: sharedRun, transcripts }; return previous !== undefined && sameValue(previous, shared) ? previous : shared; }
|
|
404
401
|
function shareSubagent(previous, incoming) { const transcript = Array.isArray(incoming?.transcript) && Array.isArray(previous?.transcript) && sameValue(previous.transcript, incoming.transcript) ? previous.transcript : incoming?.transcript; const shared = { ...incoming, transcript }; return previous !== undefined && sameValue(previous, shared) ? previous : shared; }
|
|
405
402
|
function sharePublisher(previous, incoming) { const sharedRuns = Array.isArray(incoming?.runs) ? incoming.runs.map((record) => shareRunRecord(previous?.runs?.find((candidate) => candidate?.run?.id === record?.run?.id), record)) : incoming?.runs; const sharedSubagents = Array.isArray(incoming?.subagents) ? incoming.subagents.map((subagent) => { const prior = previous?.subagents?.find((candidate) => candidate?.id === subagent?.id); return shareSubagent(prior, subagent); }) : incoming?.subagents; const shared = { ...incoming, runs: sharedRuns, subagents: sharedSubagents }; return previous !== undefined && sameValue(previous, shared) ? previous : shared; }
|
|
@@ -431,12 +428,18 @@
|
|
|
431
428
|
const runTokens = (record) => record.run.usage?.tokens ?? record.run.agents.reduce((sum, agent) => sum + accounting(agent).input + accounting(agent).output, 0);
|
|
432
429
|
const startOf = (record) => { const times = record.run.agents.map((agent) => agent.startedAt).filter((time) => Number.isFinite(time)); const created = Date.parse(record.createdAt || ""); return new Date(Math.min(...times, Number.isFinite(created) ? created : Date.now())); };
|
|
433
430
|
const endOf = (record) => { const start = startOf(record).getTime(); const ends = record.run.agents.map((agent) => agent.startedAt && agent.durationMs ? agent.startedAt + agent.durationMs : 0); return Math.max(start + 1, ...ends, record.run.state === "running" || record.run.state === "paused" || record.run.state === "awaiting_input" ? Date.now() : start + (record.run.usage?.durationMs || 0)); };
|
|
431
|
+
// Newest completion first; runs still going have no end yet, so they stay on top, newest start first.
|
|
432
|
+
const isActiveRun = (record) => ["running", "paused", "awaiting_input"].includes(record.run.state);
|
|
433
|
+
const runsByCompletion = (publisher) => { if (!Array.isArray(publisher.runs)) return publisher; const keyed = publisher.runs.map((record) => ({ record, end: isActiveRun(record) ? Infinity : endOf(record), start: startOf(record).getTime() })); keyed.sort((left, right) => right.end - left.end || right.start - left.start); return { ...publisher, runs: keyed.map(({ record }) => record) }; };
|
|
434
434
|
const attention = (record) => (record.awaiting || []).length + record.run.agents.filter((agent) => agent.state === "failed").length;
|
|
435
435
|
const duration = (record) => Math.max(0, endOf(record) - startOf(record).getTime());
|
|
436
436
|
const age = (stamp) => { const minutes = Math.floor((Date.now() - stamp) / 60000); return minutes < 60 ? `${minutes}m ago` : `${Math.floor(minutes / 60)}h ago`; };
|
|
437
|
-
function tickClocks() { const now = Date.now(); document.querySelectorAll("[data-clock-kind]").forEach((element) => { const start = Number(element.dataset.clockStart); if (!Number.isFinite(start)) return; const end = element.dataset.clockEnd === "live" ? now : Number(element.dataset.clockEnd); const value = element.dataset.clockKind === "age" ? age(start) : fmtRuntime((Number.isFinite(end) ? end : now) - start); const prefix = element.dataset.clockPrefix; const text = prefix ? `${prefix} · ${value}` : value; if (element.firstChild) element.firstChild.nodeValue = text; else element.append(text); if (element.dataset.clockTitlePrefix) element.title = `${element.dataset.clockTitlePrefix} · ${value}`; }); const swim = $("swim"); if (swim?.dataset.timelineRunning !== "true") return; const start = Number(swim.dataset.timelineStart); if (!Number.isFinite(start)) return; const span = Math.max(1, now - start); swim.dataset.timelineSpan = String(span); swim.querySelectorAll("[data-clock-bar-start]").forEach((bar) => { const barStart = Number(bar.dataset.clockBarStart); const minimum = Number(bar.dataset.clockBarMin); if (Number.isFinite(barStart)) bar.style.width = `${Math.max(Number.isFinite(minimum) ? minimum : 0, (now - barStart) / span * 100)}%`; });
|
|
438
|
-
|
|
439
|
-
|
|
437
|
+
function tickClocks() { const now = Date.now(); document.querySelectorAll("[data-clock-kind]").forEach((element) => { const start = Number(element.dataset.clockStart); if (!Number.isFinite(start)) return; const end = element.dataset.clockEnd === "live" ? now : Number(element.dataset.clockEnd); const value = element.dataset.clockKind === "age" ? age(start) : fmtRuntime((Number.isFinite(end) ? end : now) - start); const prefix = element.dataset.clockPrefix; const text = prefix ? `${prefix} · ${value}` : value; if (element.firstChild) element.firstChild.nodeValue = text; else element.append(text); if (element.dataset.clockTitlePrefix) element.title = `${element.dataset.clockTitlePrefix} · ${value}`; }); const swim = $("swim"); if (swim?.dataset.timelineRunning !== "true") return; const start = Number(swim.dataset.timelineStart); if (!Number.isFinite(start)) return; const span = quantizeSpan(Math.max(1, now - start)); swim.dataset.timelineSpan = String(span); swim.querySelectorAll("[data-clock-bar-start]").forEach((bar) => { const barStart = Number(bar.dataset.clockBarStart); const minimum = Number(bar.dataset.clockBarMin); if (Number.isFinite(barStart)) bar.style.width = `${Math.max(Number.isFinite(minimum) ? minimum : 0, (now - barStart) / span * 100)}%`; }); setAxisLabel(swim.querySelector('[data-clock-axis="mid"]'), `+${fmtRuntime(span / 2)}`); setAxisLabel(swim.querySelector('[data-clock-axis="end"]'), fmtClock(now)); }
|
|
438
|
+
function setAxisLabel(element, text) { if (!element) return; if (element.firstChild) element.firstChild.nodeValue = text; else element.append(text); }
|
|
439
|
+
// Prism tokenises whole sources, so multi-line strings and comments keep their colour; unknown languages stay plain text.
|
|
440
|
+
const highlightCode = (source, language) => { const grammar = globalThis.Prism?.languages[language]; return grammar ? Prism.highlight(String(source ?? ""), grammar, language) : esc(source); };
|
|
441
|
+
// Token spans can cross lines, so each numbered line reopens the spans still open from the line before.
|
|
442
|
+
const numbered = (source) => { const open = []; return highlightCode(source, "javascript").split("\n").map((line, index) => { const prefix = open.join(""); for (const [tag] of line.matchAll(/<span[^>]*>|<\/span>/g)) { if (tag === "</span>") open.pop(); else open.push(tag); } return `<div class="ln"><span class="no">${index + 1}</span><span>${prefix}${line}${"</span>".repeat(open.length)}</span></div>`; }).join(""); };
|
|
440
443
|
const contentText = (value) => Array.isArray(value) ? value.map((part) => typeof part === "string" ? part : part?.text || part?.thinking || part?.name || "").join(" ") : typeof value === "string" ? value : value ? json(value) : "";
|
|
441
444
|
const assistantBodyText = (value) => Array.isArray(value) ? value.filter((part) => typeof part === "string" || part?.type === "text" || part?.type === "thinking").map((part) => typeof part === "string" ? part : part.type === "text" ? part.text || "" : part.thinking || "").join(" ") : contentText(value);
|
|
442
445
|
const toolCallsOf = (entry) => { const content = entry?.message?.content; return Array.isArray(content) ? content.filter((part) => part && part.type === "toolCall") : []; };
|
|
@@ -464,6 +467,9 @@
|
|
|
464
467
|
const safeClass = name !== "class" || (tag === "code" && /^(?:language|lang)-[a-z0-9_-]+(?:\s+(?:language|lang)-[a-z0-9_-]+)*$/i.test(value));
|
|
465
468
|
if (name.startsWith("on") || !markdownAllowedAttributes.has(name) || name === "style" || !safeClass || ((name === "href" || name === "src") && /^(?:javascript|data):/i.test(value.replace(/[\u0000-\u0020]/g, "")))) element.removeAttribute(attribute.name);
|
|
466
469
|
}
|
|
470
|
+
// Fenced code keeps its language class through sanitising; Prism then rebuilds the block from its text.
|
|
471
|
+
const language = tag === "code" && globalThis.Prism ? /(?:language|lang)-([a-z0-9_-]+)/i.exec(element.getAttribute("class") || "")?.[1].toLowerCase() : undefined;
|
|
472
|
+
if (language && Prism.languages[language]) element.innerHTML = highlightCode(element.textContent, language);
|
|
467
473
|
}
|
|
468
474
|
return parsed.body.innerHTML;
|
|
469
475
|
}
|
|
@@ -479,7 +485,7 @@
|
|
|
479
485
|
const transcriptEmptyMessage = (key) => ({ empty: "No transcript events recorded.", missing: "Transcript not found.", disconnected: "Publisher disconnected.", failed: "Transcript unavailable.", oversized: "Transcript is too large." }[state.transcriptStatus[key]] || "Transcript unavailable.");
|
|
480
486
|
const toolArgsOf = (entry, entries) => { const id = toolCallIdOf(entry); if (id) { for (const candidate of entries) { const content = candidate.message?.content; if (!Array.isArray(content)) continue; const call = content.find((part) => part && part.type === "toolCall" && part.id === id); if (call && call.arguments !== undefined) return call.arguments; } } return entry.message?.arguments !== undefined ? entry.message.arguments : entry.arguments !== undefined ? entry.arguments : {}; };
|
|
481
487
|
const toolDescriptionOf = (agent, name) => (agent.toolDefinitions || []).find((tool) => tool.name === name)?.description || "";
|
|
482
|
-
const sessionResources = (agent) => { const snapshot = selected()?.record.snapshot; const resources = agent?.attemptDetails?.at(-1)?.setup?.resourceSelectors; return { skillFilters: resources?.selectors?.skills?.length ? resources.selectors.skills : snapshot?.settings?.skills || [], skillResolved: resources?.skills || [], extensionFilters: resources?.selectors?.extensions?.length ? resources.selectors.extensions : snapshot?.settings?.extensions || [], extensionResolved: resources?.extensions || [] }; };
|
|
488
|
+
const sessionResources = (agent) => { const snapshot = selected()?.record.snapshot; const resources = agent?.attemptDetails?.at(-1)?.setup?.resourceSelectors; return { skillFilters: resources?.selectors?.skills?.length ? resources.selectors.skills : agent?.skills?.length ? agent.skills : snapshot?.settings?.skills || [], skillResolved: resources?.skills || [], extensionFilters: resources?.selectors?.extensions?.length ? resources.selectors.extensions : agent?.extensions?.length ? agent.extensions : snapshot?.settings?.extensions || [], extensionResolved: resources?.extensions || [] }; };
|
|
483
489
|
const resourceTitle = (value) => { const parts = String(value).replaceAll("\\", "/").split("/").filter(Boolean); const file = parts.at(-1) || String(value); if (/^(skill\.md|index\.(ts|js|mjs))$/i.test(file)) return parts.at(-2) || file; return file.replace(/\.(ts|js|mjs)$/i, ""); };
|
|
484
490
|
const nameList = (items) => items.length ? items.map((item) => `<div class="tool-item"><b>${esc(item)}</b></div>`).join("") : '<div class="hint">None recorded.</div>';
|
|
485
491
|
const resourceList = (items) => items.length ? items.map((item) => { const title = resourceTitle(item); return `<div class="tool-item"><b>${esc(title)}</b>${title !== item ? `<span>${esc(item)}</span>` : ""}</div>`; }).join("") : '<div class="hint">None resolved.</div>';
|
|
@@ -576,21 +582,15 @@
|
|
|
576
582
|
const eventSearchText = (entry, entries = []) => { const toolCalls = toolCallsOf(entry); if (toolCalls.length) return toolCalls.map((call) => `${call.name} ${JSON.stringify(toolArgsOf(call, entries))}`).join(" "); return eventKind(entry) === "tool" ? `${toolNameOf(entry)} ${JSON.stringify(toolArgsOf(entry, entries))}` : ""; };
|
|
577
583
|
const renderAssistantPreview = (preview) => { const text = preview.text ? `<span>${esc(preview.text)}</span>` : ""; const separator = text && preview.names.length ? '<span class="preview-sep"> | </span>' : ""; const names = preview.names.slice(0, 3).map((name) => `<span class="pill tool preview-tool">${esc(name)}</span>`).join(""); const overflow = preview.overflow ? `<span class="preview-overflow">+${preview.overflow} others</span>` : ""; return `${text}${separator}${names}${overflow}`; };
|
|
578
584
|
const eventLabel = (kind) => kind === "toolResult" ? "TOOL" : kind === "context" ? "RESULT" : kind === "issue" ? "FAILURE" : String(kind || "EVENT").toUpperCase();
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
document.documentElement.dataset.theme = button.dataset.theme;
|
|
589
|
-
try { localStorage.setItem("traj-theme", button.dataset.theme); } catch { /* Preferences are optional. */ }
|
|
590
|
-
renderThemeButtons();
|
|
591
|
-
}));
|
|
592
|
-
const current = document.documentElement.dataset.theme || "tty";
|
|
593
|
-
$("theme-buttons").querySelectorAll("[data-theme]").forEach((button) => button.classList.toggle("on", button.dataset.theme === current));
|
|
585
|
+
// Without a pinned choice the page follows the system scheme; the toggle cycles system, light, and dark.
|
|
586
|
+
const themeModes = { auto: { next: "light", icon: "\u25D0", label: "Theme: system" }, light: { next: "dark", icon: "\u2600", label: "Theme: light" }, dark: { next: "auto", icon: "\u263E", label: "Theme: dark" } };
|
|
587
|
+
const currentTheme = () => ["light", "dark"].includes(document.documentElement.dataset.theme) ? document.documentElement.dataset.theme : "auto";
|
|
588
|
+
const themeToggleHtml = () => { const mode = themeModes[currentTheme()]; return `<button type="button" class="theme-toggle" data-theme-toggle title="${mode.label}" aria-label="${mode.label}">${mode.icon}</button>`; };
|
|
589
|
+
function toggleTheme() {
|
|
590
|
+
const next = themeModes[currentTheme()].next;
|
|
591
|
+
if (next === "auto") delete document.documentElement.dataset.theme; else document.documentElement.dataset.theme = next;
|
|
592
|
+
try { localStorage.setItem("traj-theme", next); } catch { /* Preferences are optional. */ }
|
|
593
|
+
renderSidebar();
|
|
594
594
|
}
|
|
595
595
|
const subagentAccounting = (subagent) => subagent.progress?.accounting || subagent.attempt?.accounting || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
596
596
|
const subagentCost = (subagent) => subagentAccounting(subagent).cost;
|
|
@@ -601,6 +601,15 @@
|
|
|
601
601
|
const subagentAttention = (subagent) => subagent.state === "failed" ? 1 : 0;
|
|
602
602
|
const subagentModel = (subagent) => subagent.model ? `${subagent.model.provider || "?"}/${subagent.model.model || "?"}` : "—";
|
|
603
603
|
const subagentLabel = (subagent) => subagent.label || subagent.id.slice(0, 8);
|
|
604
|
+
// A session shows every active entry and at least three in total; the rest wait behind a "more" toggle, and the selected entry always stays visible.
|
|
605
|
+
// The toggle reuses the persisted sidebar section set, where membership of this key means expanded.
|
|
606
|
+
function sidebarWindow(items, key, active, selected) {
|
|
607
|
+
const limit = Math.max(3, items.filter(active).length);
|
|
608
|
+
if (items.length <= limit) return { visible: items, more: "" };
|
|
609
|
+
const expanded = state.sidebarCollapsed.has(key);
|
|
610
|
+
const visible = expanded ? items : items.filter((item, index) => index < limit || selected(item));
|
|
611
|
+
return { visible, more: `<button type="button" class="more-items section-toggle" data-sidebar-section="${esc(key)}" aria-expanded="${expanded}">${expanded ? "show less" : `+ ${items.length - visible.length} more`}</button>` };
|
|
612
|
+
}
|
|
604
613
|
function renderSidebar() {
|
|
605
614
|
const groups = sidebarGroups(livePublishers());
|
|
606
615
|
const html = groups.map((group) => {
|
|
@@ -608,38 +617,87 @@
|
|
|
608
617
|
const collapsed = state.sidebarCollapsed.has(group.key);
|
|
609
618
|
const sessions = group.publishers.map((publisher) => {
|
|
610
619
|
const publisherAttention = (publisher.runs || []).reduce((total, record) => total + attention(record), 0) + (publisher.subagents || []).reduce((total, subagent) => total + subagentAttention(subagent), 0);
|
|
611
|
-
const
|
|
620
|
+
const selectedKey = targetKey(currentTarget() || {});
|
|
621
|
+
const runWindow = sidebarWindow(publisher.runs || [], `more:runs:${publisher.id}`, isActiveRun, (record) => selectedKey === `run:${runKey(publisher, record)}`);
|
|
622
|
+
const runs = runWindow.visible.map((record) => {
|
|
612
623
|
const ref = runKey(publisher, record);
|
|
613
624
|
const pending = record.awaiting?.length || attention(record);
|
|
614
625
|
return `<button class="run-item${targetKey(currentTarget() || {}) === `run:${ref}` ? " on" : ""}" data-run="${esc(ref)}" data-publisher="${esc(publisher.id)}">${glyph(runState(record))}<span class="n">${esc(record.run.workflowName)}</span>${pending ? `<span class="await">${pending} attn</span>` : ""}<span class="tel" data-clock-kind="${record.run.state === "running" ? "runtime" : "age"}" data-clock-start="${startOf(record).getTime()}" data-clock-prefix="${esc(fmtCost(runCost(record)))}" data-clock-title-prefix="${esc(`${runState(record)} · ${fmtCost(runCost(record))}`)}" title="${esc(`${runState(record)} · ${fmtCost(runCost(record))} · ${fmtRuntime(duration(record))}`)}">${fmtCost(runCost(record))} · ${record.run.state === "running" ? fmtRuntime(Date.now() - startOf(record).getTime()) : age(startOf(record).getTime())}</span></button>`;
|
|
615
|
-
}).join("");
|
|
626
|
+
}).join("") + runWindow.more;
|
|
616
627
|
const subagentSectionKey = `subagents:${publisher.id}`;
|
|
617
628
|
const subagentsCollapsed = state.sidebarCollapsed.has(subagentSectionKey);
|
|
618
|
-
const
|
|
629
|
+
const subagentWindow = sidebarWindow(publisher.subagents || [], `more:subagents:${publisher.id}`, (subagent) => subagent.state === "running", (subagent) => selectedKey === targetKey({ kind: "subagent", publisherId: publisher.id, id: subagent.id }));
|
|
630
|
+
const subagents = subagentWindow.visible.map((subagent) => {
|
|
619
631
|
const ref = subagentKey(publisher, subagent);
|
|
620
632
|
const target = { kind: "subagent", publisherId: publisher.id, id: subagent.id };
|
|
621
633
|
const pending = subagentAttention(subagent);
|
|
622
634
|
const telemetry = `${subagent.state} · ${fmtCost(subagentCost(subagent))} · ${fmtRuntime(subagentRuntime(subagent))}`;
|
|
623
635
|
return `<button class="run-item subagent${targetKey(currentTarget() || {}) === targetKey(target) ? " on" : ""}" data-subagent="${esc(ref)}" data-publisher="${esc(publisher.id)}">${glyph(subagent.state)}<span class="n" title="${esc(subagent.id)}">${esc(subagentLabel(subagent))}</span>${pending ? `<span class="await">${pending} attn</span>` : ""}<span class="tel" data-clock-kind="${subagent.state === "running" ? "runtime" : "age"}" data-clock-start="${subagentStartOf(subagent)}" data-clock-prefix="${esc(fmtCost(subagentCost(subagent)))}" data-clock-title-prefix="${esc(`${subagent.state} · ${fmtCost(subagentCost(subagent))}`)}" title="${esc(telemetry)}">${fmtCost(subagentCost(subagent))} · ${subagent.state === "running" ? fmtRuntime(subagentRuntime(subagent)) : age(subagentStartOf(subagent))}</span></button>`;
|
|
624
|
-
}).join("");
|
|
636
|
+
}).join("") + subagentWindow.more;
|
|
625
637
|
return `<div class="pub${publisher.id === state.currentPub ? " on" : ""}"><div class="row"><span class="dot live"></span><span class="name">${esc(publisher.title || publisher.id)}</span>${publisherAttention ? `<span class="attn">${publisherAttention}</span>` : ""}</div><div class="children">${runs || '<div class="hint">no runs</div>'}<button type="button" class="subagents-head section-toggle" data-sidebar-section="${esc(subagentSectionKey)}" aria-expanded="${!subagentsCollapsed}">${subagentsCollapsed ? "▸" : "▾"} SUBAGENTS <span class="count">${(publisher.subagents || []).length}</span></button>${subagentsCollapsed ? "" : subagents || '<div class="hint">no subagents</div>'}</div></div>`;
|
|
626
638
|
}).join("");
|
|
627
639
|
return `<div class="group"><button type="button" class="folder" data-sidebar-group="${esc(group.key)}" title="${esc(group.key)}" aria-expanded="${!collapsed}"><span class="marker">${collapsed ? "▸" : "▾"}</span><span class="name">${esc(group.label)}</span><span class="count">${group.publishers.length}</span>${groupAttention ? `<span class="attn">${groupAttention}</span>` : ""}</button>${collapsed ? "" : `<div class="group-children">${sessions}</div>`}</div>`;
|
|
628
640
|
}).join("");
|
|
629
|
-
patch($("sidebar"), `<div class="brand"
|
|
630
|
-
renderThemeButtons();
|
|
641
|
+
patch($("sidebar"), `<div class="brand"><span class="brand-name">Trajectory</span>${themeToggleHtml()}</div>${html}`);
|
|
631
642
|
}
|
|
632
|
-
|
|
633
|
-
|
|
643
|
+
// A live timeline grows in ~10% steps so bar geometry stays byte-identical between frames instead of shifting every second.
|
|
644
|
+
const quantizeSpan = (ms) => Math.max(1, Math.round(Math.pow(1.1, Math.ceil(Math.log(Math.max(1, ms)) / Math.log(1.1)))));
|
|
645
|
+
const timelineSpan = (record) => { const span = Math.max(1, endOf(record) - startOf(record).getTime()); return record.run.state === "running" ? quantizeSpan(span) : span; };
|
|
646
|
+
const barStyle = (offsetMs, lengthMs, minWidth, span) => `left:${Math.max(0, offsetMs / span * 100)}%;width:${Math.max(minWidth, lengthMs / span * 100)}%`;
|
|
647
|
+
// Tool calls less than a pixel apart on the track overlap on screen, so they ship as one bar instead of thousands.
|
|
648
|
+
const TOOL_BAR_MERGE_RATIO = 0.001;
|
|
649
|
+
function mergedToolBars(timings, span) {
|
|
650
|
+
const gap = span * TOOL_BAR_MERGE_RATIO;
|
|
651
|
+
const groups = [];
|
|
652
|
+
for (const { name, timing } of [...timings].sort((left, right) => left.timing.startedAt - right.timing.startedAt)) {
|
|
653
|
+
const last = groups[groups.length - 1];
|
|
654
|
+
if (last && timing.startedAt - last.end <= gap) {
|
|
655
|
+
last.end = Math.max(last.end, timing.completedAt);
|
|
656
|
+
last.count += 1;
|
|
657
|
+
last.error = last.error || timing.isError === true;
|
|
658
|
+
if (last.names.length < 4 && !last.names.includes(name)) last.names.push(name);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
groups.push({ start: timing.startedAt, end: timing.completedAt, count: 1, error: timing.isError === true, names: [name], timing });
|
|
662
|
+
}
|
|
663
|
+
return groups;
|
|
664
|
+
}
|
|
665
|
+
const toolBarTitle = (group) => group.count === 1 ? `${group.names[0]} \u00b7 ${formatToolTiming(group.timing)}` : `${group.count} tool calls \u00b7 ${group.names.join(", ")}${group.count > group.names.length ? ", …" : ""} \u00b7 ${fmtRuntime(group.end - group.start)}`;
|
|
666
|
+
function renderGantt(record, timingsByAgent) { return ganttLanes(record, timingsByAgent).map(({ html }) => html).join(""); }
|
|
667
|
+
function ganttLanes(record, timingsByAgent) {
|
|
668
|
+
const agents = record.run.agents; const start = startOf(record).getTime(); const live = record.run.state === "running"; const span = timelineSpan(record); const lanes = []; let previous = "";
|
|
634
669
|
for (const agent of agents) {
|
|
635
670
|
const crumb = agent.parentBreadcrumb || "";
|
|
636
|
-
if (crumb && crumb !== previous) { const group = agents.filter((candidate) => (candidate.parentBreadcrumb || "") === crumb); const left = Math.min(...group.map((candidate) => (
|
|
671
|
+
if (crumb && crumb !== previous) { const group = agents.filter((candidate) => (candidate.parentBreadcrumb || "") === crumb); const left = Math.min(...group.map((candidate) => (candidate.startedAt || start) - start)); const right = Math.max(...group.map((candidate) => (candidate.startedAt || start) + (candidate.durationMs || 0) - start)); lanes.push({ key: `crumb:${crumb}`, html: `<div class="lane crumb"><span></span><div class="lab">${esc(crumb)}</div><div class="track"><i class="bar crumb" style="${barStyle(Math.max(0, left), Math.max(0, right - left), 1, span)}"></i></div></div>` }); }
|
|
637
672
|
previous = crumb;
|
|
638
|
-
const timings = timingsByAgent.get(agent.id) || []; const left = agent.startedAt ?
|
|
639
|
-
const toolBars = timings
|
|
640
|
-
|
|
673
|
+
const timings = timingsByAgent.get(agent.id) || []; const left = agent.startedAt ? agent.startedAt - start : 0; const running = agent.state === "running";
|
|
674
|
+
const toolBars = mergedToolBars(timings, span).map((group) => `<i class="bar tool ${group.error ? "fail" : "ok"}" style="${barStyle(group.start - start, group.end - group.start, 0.5, span)}" title="${esc(toolBarTitle(group))}"></i>`).join("");
|
|
675
|
+
// A running agent's width is time-dependent, so tickClocks owns it and the markup stays stable between frames.
|
|
676
|
+
const style = running ? `left:${Math.max(0, left / span * 100)}%;width:0%` : barStyle(Math.max(0, left), agent.durationMs || 0, 0, span);
|
|
677
|
+
lanes.push({ key: `agent:${agent.id}`, html: `<div class="lane${agent.parentId ? " child" : ""}" data-agent="${esc(agent.id)}">${glyph(agent.state)}<div class="lab">${esc(agent.label || agent.name)}</div><div class="track"><i class="bar ${stateClass(agent.state)}" style="${style}"${running ? ` data-clock-bar-start="${agent.startedAt || start}" data-clock-bar-min="2"` : ""}></i>${toolBars}</div></div>` });
|
|
678
|
+
}
|
|
679
|
+
return [{ key: "axis", html: `<div class="axis"><div></div><div class="ticks"><span>${fmtClock(start)}</span><span data-clock-axis="mid">${live ? "" : `+${fmtRuntime(span / 2)}`}</span><span data-clock-axis="end">${live ? "" : "end"}</span></div></div>` }, ...lanes];
|
|
680
|
+
}
|
|
681
|
+
// Only the lanes whose markup changed are re-parsed; the quantised span keeps the others byte-identical between frames.
|
|
682
|
+
let ganttOrder = "";
|
|
683
|
+
const ganttLaneHtml = new Map();
|
|
684
|
+
function patchGantt(record, timingsByAgent) {
|
|
685
|
+
const root = $("swim-content");
|
|
686
|
+
const lanes = ganttLanes(record, timingsByAgent);
|
|
687
|
+
const order = `${state.currentRun}\n${lanes.map(({ key }) => key).join("\n")}`;
|
|
688
|
+
if (order !== ganttOrder || root.children.length !== lanes.length) {
|
|
689
|
+
ganttOrder = order;
|
|
690
|
+
ganttLaneHtml.clear();
|
|
691
|
+
patch(root, lanes.map(({ html }) => html).join(""));
|
|
692
|
+
for (const { key, html } of lanes) ganttLaneHtml.set(key, html);
|
|
693
|
+
return;
|
|
641
694
|
}
|
|
642
|
-
|
|
695
|
+
lanes.forEach(({ key, html }, index) => {
|
|
696
|
+
if (ganttLaneHtml.get(key) === html) return;
|
|
697
|
+
ganttLaneHtml.set(key, html);
|
|
698
|
+
const child = root.children[index];
|
|
699
|
+
if (child) child.outerHTML = html;
|
|
700
|
+
});
|
|
643
701
|
}
|
|
644
702
|
function renderSubagentGantt(publisher) {
|
|
645
703
|
const subagents = publisher.subagents || []; if (!subagents.length) return '<div class="hint">No subagents recorded.</div>';
|
|
@@ -669,26 +727,58 @@
|
|
|
669
727
|
}
|
|
670
728
|
function renderLogs(record) { const events = record.run.events || []; const logs = events.filter((event) => event.type === "log"); const omitted = events.find((event) => event.type === "trajectory:truncated"); if (!logs.length && !omitted) return ""; const collapsed = state.runLayout.logsCollapsed; return `<section class="logs"><button type="button" class="sec section-toggle" data-toggle="logs" aria-expanded="${!collapsed}">${collapsed ? "▸" : "▾"} LOGS</button>${collapsed ? "" : `<div class="logs-list">${logs.map((event) => `<div class="log-row"><span class="log-time">${esc(fmtClock(event.timestamp))}</span><span class="log-message">${esc(event.message)}</span></div>`).join("")}${omitted ? `<div class="log-row"><span class="log-time">—</span><span class="log-message">${esc(omitted.omitted)} events omitted from live state</span></div>` : ""}</div>`}</section>`; }
|
|
671
729
|
function renderAgentSection(record, timingsByAgent) { const collapsed = state.runLayout.agentsCollapsed; return `<button type="button" class="sec section-toggle" data-toggle="agents" aria-expanded="${!collapsed}">${collapsed ? "▸" : "▾"} AGENTS</button>${collapsed ? "" : renderAgentGrid(record, timingsByAgent)}`; }
|
|
672
|
-
function renderList(record, timingsByAgent) { const run = record.run; const completed = run.agents.filter((agent) => agent.state === "completed").length; return `<section class="workflow-card"><button type="button" class="workflow-head on" data-run="${esc(state.currentRun)}">${glyph(run.state)}<span class="workflow-name">${esc(run.workflowName)}</span><span class="workflow-meta">${completed}/${run.agents.length} agents · ${fmtCost(runCost(record))} · <span data-clock-kind="runtime" data-clock-start="${startOf(record).getTime()}" data-clock-end="${run.state === "running" ? "live" : endOf(record)}"
|
|
730
|
+
function renderList(record, timingsByAgent) { const run = record.run; const completed = run.agents.filter((agent) => agent.state === "completed").length; return `<section class="workflow-card"><button type="button" class="workflow-head on" data-run="${esc(state.currentRun)}">${glyph(run.state)}<span class="workflow-name">${esc(run.workflowName)}</span><span class="workflow-meta">${completed}/${run.agents.length} agents · ${fmtCost(runCost(record))} · <span data-clock-kind="runtime" data-clock-start="${startOf(record).getTime()}" data-clock-end="${run.state === "running" ? "live" : endOf(record)}"></span></span></button>${renderAgentSection(record, timingsByAgent)}${renderLogs(record)}</section>`; }
|
|
673
731
|
function actionButton(label, action, disabled) { return `<button class="btn-alt" data-act="${action}"${disabled ? " disabled" : ""}>${label}</button>`; }
|
|
674
|
-
function subagentAgent(subagent) { return { id: subagent.id, name: subagentLabel(subagent), label: subagentLabel(subagent), role: subagent.role || "subagent", state: subagent.state, model: subagent.model, tools: subagent.tools || [], toolDefinitions: subagent.toolDefinitions || [], prompt: subagent.request.prompt, systemPrompt: subagent.attempt?.setup?.systemPrompt || subagent.request.prompt, attempts: subagent.attempts || 0, mode: subagent.mode, worktree: subagent.worktree, outcome: subagent.result !== undefined ? { kind: "result", value: subagent.result } : subagent.failure !== undefined ? { kind: "failure", value: subagent.failure } : subagent.attempt?.error !== undefined ? { kind: "failure", value: subagent.attempt.error } : undefined, finishedAt: subagent.finishedAt, attemptDetails: subagent.attempt ? [subagent.attempt] : [] }; }
|
|
732
|
+
function subagentAgent(subagent) { return { id: subagent.id, name: subagentLabel(subagent), label: subagentLabel(subagent), role: subagent.role || "subagent", state: subagent.state, model: subagent.model, requestedModel: subagent.request.model, tools: subagent.tools || [], toolDefinitions: subagent.toolDefinitions || [], skills: subagent.request.skills || [], extensions: subagent.request.extensions || [], contextFiles: subagent.request.contextFiles || [], prompt: subagent.request.prompt, systemPrompt: subagent.attempt?.setup?.systemPrompt || subagent.request.prompt, attempts: subagent.attempts || 0, mode: subagent.mode, cwd: subagent.cwd, worktree: subagent.worktree, output: subagent.output || subagentOutputFallback(subagent), outcome: subagent.result !== undefined ? { kind: "result", value: subagent.result } : subagent.failure !== undefined ? { kind: "failure", value: subagent.failure } : subagent.attempt?.error !== undefined ? { kind: "failure", value: subagent.attempt.error } : undefined, finishedAt: subagent.finishedAt, attemptDetails: subagent.attempt ? [subagent.attempt] : [] }; }
|
|
733
|
+
function subagentOutputFallback(subagent) { if (subagent.result !== undefined) return { status: "available", value: subagent.result, bytes: 0 }; if (subagent.failure !== undefined) return { status: "failed", code: subagent.failure.code || "AGENT_FAILED", message: subagent.failure.message || "Subagent failed" }; if (subagent.state === "running") return { status: "pending" }; if (subagent.state === "completed") return { status: "unavailable" }; if (subagent.state === "stopped") return { status: "cancelled", code: subagent.error?.code || "CANCELLED", message: subagent.error?.message || "Subagent stopped" }; if (subagent.state === "failed") return { status: "failed", code: subagent.error?.code || "AGENT_FAILED", message: subagent.error?.message || "Subagent failed" }; return { status: "unavailable" }; }
|
|
675
734
|
function renderAgentStats(found, agent, isSubagent) {
|
|
676
735
|
const offline = !found.publisher.connected; const stateText = agent.state || "—"; const value = isSubagent ? subagentAccounting(found.record) : accounting(agent);
|
|
677
736
|
const startedAt = isSubagent ? subagentStartOf(found.record) : agent.startedAt; const endedAt = stateText === "running" ? undefined : isSubagent ? subagentEndOf(found.record) : Number.isFinite(agent.durationMs) && agent.startedAt ? agent.startedAt + agent.durationMs : undefined;
|
|
678
737
|
const clock = Number.isFinite(startedAt) ? `<span data-clock-kind="runtime" data-clock-start="${startedAt}" data-clock-end="${stateText === "running" ? "live" : Number.isFinite(endedAt) ? endedAt : startedAt}">${esc(fmtRuntime(Math.max(0, (Number.isFinite(endedAt) ? endedAt : Date.now()) - startedAt)))}</span>` : "—";
|
|
679
738
|
const controls = isSubagent ? stateText === "running" ? `${actionButton("Stop", "stop", offline)}<input class="steer-input" id="subagent-steer" placeholder="Send guidance"${offline ? " disabled" : ""} /><button class="btn-alt" data-act="steer"${offline ? " disabled" : ""}>Send</button>` : stateText === "failed" || stateText === "stopped" ? actionButton("Retry", "retry", offline) : "" : "";
|
|
680
|
-
return `${glyph(stateText)} ${fmtCost(value.cost)} · ${fmtTokens(value.input + value.output)} · runtime ${clock}${controls ? `<span class="actions">${controls}</span>` : ""}${isSubagent && offline ? '<span class="offline">publisher offline — actions disabled</span>' : ""}`;
|
|
739
|
+
return `${glyph(stateText)} <button type="button" class="btn-alt agent-details" data-agent-details="1">Agent details</button> ${fmtCost(value.cost)} · ${fmtTokens(value.input + value.output)} · runtime ${clock}${controls ? `<span class="actions">${controls}</span>` : ""}${isSubagent && offline ? '<span class="offline">publisher offline — actions disabled</span>' : ""}`;
|
|
681
740
|
}
|
|
682
|
-
function
|
|
741
|
+
function renderWorkflowArguments(record, open = false, key = "") { const args = record.snapshot?.args; const incomplete = record.snapshotArgsTruncated === true; if (args === null || args === undefined && !incomplete) return ""; const status = incomplete ? ' <span class="ins-sub">incomplete</span>' : ""; const body = args === undefined ? '<div class="hint">Arguments unavailable in the live transport.</div>' : `<div class="code args">${highlightCode(json(args), "json")}</div>`; const warning = incomplete ? '<div class="workflow-args-warning">Arguments were truncated by the live transport; this value is incomplete.</div>' : ""; return `<details class="workflow-args"${open ? " open" : ""} data-workflow-args="${esc(key)}"><summary class="sec">ARGUMENTS${status}</summary>${body}${warning}</details>`; }
|
|
742
|
+
function renderDossier(publisher, record) { const offline = !publisher.connected; const issues = record.run.agents.filter((agent) => agent.state === "failed").map((agent) => `<div class="pending issue"><div class="ttl">${glyph(agent.state)} ${esc(agent.label || agent.name)} needs retry</div><div class="ins-sub">${esc(agent.attemptDetails?.at(-1)?.error?.message || record.run.error?.message || "agent failed")}</div><div class="actions">${actionButton("Retry", "retry", offline)}</div>${offline ? '<div class="offline">publisher offline — actions disabled</div>' : ""}</div>`).join(""); const pending = issues + (record.awaiting || []).map((item) => `<div class="pending" data-name="${esc(item.name)}"><div class="ttl">${glyph("awaiting_input")} checkpoint ${esc(item.name)}</div><div class="ins-sub">${esc(item.prompt)}</div><div class="actions">${actionButton("Approve", "checkpoint-approve", offline)}${actionButton("Reject", "checkpoint-reject", offline)}</div>${offline ? '<div class="offline">publisher offline — actions disabled</div>' : ""}</div>`).join(""); const run = record.run; const controls = (run.state === "running" || run.state === "awaiting_input" ? `${actionButton("Pause", "pause", offline)}${actionButton("Abort run", "stop", offline)}` : run.state === "pausing" ? `${actionButton("Cancel pause", "resume", offline)}${actionButton("Abort run", "stop", offline)}` : run.state === "paused" || run.state === "interrupted" || run.state === "budget_exhausted" ? `${actionButton("Resume", "resume", offline)}${actionButton("Abort run", "stop", offline)}` : "") + (window.__PIEWF_STATIC__ ? "" : actionButton("Share", "share", offline)); const rows = run.agents.map((agent) => `<tr data-agent="${esc(agent.id)}"><td>${glyph(agent.state)}</td><td><b>${esc(agent.label || agent.name)}</b><div class="ins-sub">${esc(agent.role || "agent")}</div></td><td class="num">${fmtTokens(accounting(agent).input + accounting(agent).output)}</td><td class="num">${fmtCost(accounting(agent).cost)}</td></tr>`).join(""); const script = record.snapshot?.script ? `<div class="sec">SCRIPT <span class="ins-sub">click to expand</span></div><div class="code script" data-script="1">${highlightCode(record.snapshot.script, "javascript")}</div>` : ""; const argumentsSection = renderWorkflowArguments(record, state.workflowArgumentsOpen.has(state.currentRun), state.currentRun); return `<div class="ins-head" data-run-panel="1">${glyph(run.state)}<b>${esc(run.workflowName)}</b><span class="ins-sub">${esc(run.state)}</span></div><div class="ins-body">${pending ? '<div class="sec">NEEDS ATTENTION</div>' + pending : ""}<div class="sec">STATE</div><div class="kv"><div class="k">Phase</div><div>${esc(run.phase || "—")}</div><div class="k">Active</div><div>${run.agents.filter((agent) => agent.state === "running").length} running · ${run.agents.filter((agent) => agent.state === "queued").length} queued · ${run.agents.filter((agent) => agent.state === "failed").length} failed</div><div class="k">Outcome</div><div>${esc(run.error ? `${run.error.code}: ${run.error.message}` : run.state === "completed" ? "completed" : attention(record) ? `${attention(record)} agent(s) need a human` : "—")}</div><div class="k">Cost</div><div>${fmtCost(runCost(record))}${run.budget?.costUsd?.hard !== undefined ? ` / ${fmtCost(run.budget.costUsd.hard)} hard` : ""}</div><div class="k">Tokens</div><div>${fmtTokens(runTokens(record))}${run.budget?.tokens?.hard !== undefined ? ` / ${fmtTokens(run.budget.tokens.hard)} hard` : ""}</div><div class="k">Started</div><div>${fmtClock(startOf(record))}</div><div class="k">Runtime</div><div data-clock-kind="runtime" data-clock-start="${startOf(record).getTime()}" data-clock-end="${run.state === "running" ? "live" : endOf(record)}"></div></div><div class="actions">${controls}</div>${state.shareUrls[run.id] ? `<div class="share-url"><input readonly value="${esc(state.shareUrls[run.id])}" /><button class="btn-alt" data-copy-share="${esc(state.shareUrls[run.id])}">Copy</button></div><div class="ins-sub">secret gist: anyone with the link can read it</div>` : ""}${script}${argumentsSection}<div class="sec">AGENTS</div><table class="acct"><thead><tr><th></th><th>agent</th><th class="num">tok</th><th class="num">cost</th></tr></thead><tbody>${rows}</tbody></table></div>`; }
|
|
683
743
|
function renderRunLayout() { const ganttCollapsed = state.runLayout.ganttCollapsed; const splitSwim = $("split-swim"); $("swim").classList.toggle("hidden", ganttCollapsed); splitSwim.classList.toggle("hidden", ganttCollapsed); const heading = $("toggle-gantt"); heading.textContent = `${ganttCollapsed ? "▸" : "▾"} GANTT`; heading.setAttribute("aria-expanded", String(!ganttCollapsed)); const swimHeight = Math.min(state.runLayout.swimHeight, maxSwimHeight()); document.documentElement.style.setProperty("--swim", `${swimHeight}px`); }
|
|
684
744
|
function clampSwimHeight() { renderRunLayout(); }
|
|
685
|
-
function renderHome() { const homePublisher = livePublishers().find((publisher) => publisher.id === state.currentPub); const hasSubagents = Boolean(homePublisher?.subagents?.length); $("canvas-pub").textContent = homePublisher ? homePublisher.id.slice(0, 8) : "—"; $("canvas-run").textContent = hasSubagents ? "SUBAGENTS" : "—"; patch($("swim-content"), hasSubagents ? renderSubagentGantt(homePublisher) : ""); $("swim").dataset.timelineHasTime = hasSubagents ? "true" : "false"; $("swim").dataset.timelineRunning = String(Boolean(hasSubagents && homePublisher.subagents.some((subagent) => subagent.state === "running"))); if (hasSubagents) { const subagents = homePublisher.subagents || []; const start = Math.min(...subagents.map((subagent) => subagentStartOf(subagent))); const end = Math.max(start + 1, ...subagents.map((subagent) => subagentEndOf(subagent))); $("swim").dataset.timelineStart = String(start); $("swim").dataset.timelineSpan = String(Math.max(1, end - start)); $("swim").querySelector(":scope > .now")?.classList.remove("hidden"); } else { $("swim").querySelector(":scope > .now")?.classList.add("hidden"); } patch($("list"), hasSubagents ? '<div class="hint">Select a subagent to inspect its transcript.</div>' : ""); patch($("run-stats"), hasSubagents ? `${homePublisher.subagents.length} subagents` : ""); $("run-crumb").textContent = "—"; $("agent-crumb").textContent = "—"; patch($("agent-ticks"), ""); $("agent-timeline").dataset.timelineHasTime = "false"; $("agent-timeline").querySelector(":scope > .now")?.classList.add("hidden"); $("agent-timeline").querySelector(":scope > .brush")?.classList.add("hidden"); patch($("events"), ""); $("q").value = ""; $("reset-agent-range").classList.add("hidden"); patch($("insp"), hasSubagents ? '<div class="ins-body hint">Select a subagent.</div>' : '<div class="ins-body hint">Select a run.</div>'); state.inspSig = null; renderRunLayout(); }
|
|
686
|
-
|
|
745
|
+
function renderHome() { const homePublisher = livePublishers().find((publisher) => publisher.id === state.currentPub); const hasSubagents = Boolean(homePublisher?.subagents?.length); $("canvas-pub").textContent = homePublisher ? homePublisher.id.slice(0, 8) : "—"; $("canvas-run").textContent = hasSubagents ? "SUBAGENTS" : "—"; renderedRunHtml.clear(); ganttOrder = ""; patch($("swim-content"), hasSubagents ? renderSubagentGantt(homePublisher) : ""); $("swim").dataset.timelineHasTime = hasSubagents ? "true" : "false"; $("swim").dataset.timelineRunning = String(Boolean(hasSubagents && homePublisher.subagents.some((subagent) => subagent.state === "running"))); if (hasSubagents) { const subagents = homePublisher.subagents || []; const start = Math.min(...subagents.map((subagent) => subagentStartOf(subagent))); const end = Math.max(start + 1, ...subagents.map((subagent) => subagentEndOf(subagent))); $("swim").dataset.timelineStart = String(start); $("swim").dataset.timelineSpan = String(Math.max(1, end - start)); $("swim").querySelector(":scope > .now")?.classList.remove("hidden"); } else { $("swim").querySelector(":scope > .now")?.classList.add("hidden"); } patch($("list"), hasSubagents ? '<div class="hint">Select a subagent to inspect its transcript.</div>' : ""); patch($("run-stats"), hasSubagents ? `${homePublisher.subagents.length} subagents` : ""); $("run-crumb").textContent = "—"; $("agent-crumb").textContent = "—"; patch($("agent-ticks"), ""); $("agent-timeline").dataset.timelineHasTime = "false"; $("agent-timeline").querySelector(":scope > .now")?.classList.add("hidden"); $("agent-timeline").querySelector(":scope > .brush")?.classList.add("hidden"); patch($("events"), ""); $("q").value = ""; $("reset-agent-range").classList.add("hidden"); patch($("insp"), hasSubagents ? '<div class="ins-body hint">Select a subagent.</div>' : '<div class="ins-body hint">Select a run.</div>'); state.inspSig = null; renderRunLayout(); }
|
|
746
|
+
// Live updates repeat identical markup for everything but the clocks, so each run panel is patched only when its HTML actually changes.
|
|
747
|
+
const renderedRunHtml = new Map();
|
|
748
|
+
function patchRun(id, html) { const key = `${document.body.dataset.view}\n${state.currentRun}\n${html}`; if (renderedRunHtml.get(id) === key) return; renderedRunHtml.set(id, key); patch($(id), html); }
|
|
749
|
+
function renderRun() { const found = selected(); if (!found || found.target.kind !== "run") { renderHome(); return; } const dossierBody = $("insp").querySelector(".ins-body"); const dossierScroll = dossierBody ? dossierBody.scrollTop : 0; const { publisher, record } = found; const timingsByAgent = agentTimingMap(record); $("canvas-pub").textContent = publisher.id.slice(0, 8); $("canvas-run").textContent = record.run.workflowName; patchGantt(record, timingsByAgent); $("swim").dataset.timelineStart = String(startOf(record).getTime()); $("swim").dataset.timelineSpan = String(timelineSpan(record)); $("swim").dataset.timelineHasTime = "true"; $("swim").dataset.timelineRunning = String(record.run.state === "running"); patchRun("list", renderList(record, timingsByAgent)); renderRunLayout(); patchRun("insp", renderDossier(publisher, record)); const nextDossierBody = $("insp").querySelector(".ins-body"); if (nextDossierBody && dossierScroll) nextDossierBody.scrollTop = dossierScroll; patchRun("run-stats", `${glyph(record.run.state)} ${fmtCost(runCost(record))} · ${fmtTokens(runTokens(record))} · started ${fmtClock(startOf(record))} · runtime <span data-clock-kind="runtime" data-clock-start="${startOf(record).getTime()}" data-clock-end="${record.run.state === "running" ? "live" : endOf(record)}"></span>`); tickClocks(); }
|
|
687
750
|
function entryDetails(entry, agent, entries = []) { if (compactSkillReadPreview(entry, entries) !== undefined) return { kind: "skill", entry, agent }; const kind = eventKind(entry); if (kind === "tool" || entry.type === "tool_result") return { kind: "tool", entry, agent }; if (kind === "system") return { kind: "system", entry, agent }; if (kind === "context" || kind === "issue") return { kind, entry, agent }; return { kind: kind === "user" ? "user" : "assistant", entry, agent }; }
|
|
688
|
-
function renderToolPane(detail, entries) { const entry = detail.entry; const message = entry.message || entry; const args = toolArgsOf(entry, entries); const description = toolDescriptionOf(detail.agent, toolNameOf(entry)); const timing = toolTimingOf(entry); const panes = { summary: () => `<div class="kv"><div class="k">Tool</div><div>${esc(toolNameOf(entry))}</div>${description ? `<div class="k">Description</div><div>${esc(description)}</div>` : ""}<div class="k">Hierarchy</div><div>${esc(entry.type)}</div><div class="k">Status</div><div>${toolResultIsError(entry) ? "Failed" : "Completed"}</div></div>`, payload: () => `<div class="code">${
|
|
689
|
-
function
|
|
690
|
-
function
|
|
691
|
-
|
|
751
|
+
function renderToolPane(detail, entries) { const entry = detail.entry; const message = entry.message || entry; const args = toolArgsOf(entry, entries); const description = toolDescriptionOf(detail.agent, toolNameOf(entry)); const timing = toolTimingOf(entry); const panes = { summary: () => `<div class="kv"><div class="k">Tool</div><div>${esc(toolNameOf(entry))}</div>${description ? `<div class="k">Description</div><div>${esc(description)}</div>` : ""}<div class="k">Hierarchy</div><div>${esc(entry.type)}</div><div class="k">Status</div><div>${toolResultIsError(entry) ? "Failed" : "Completed"}</div></div>`, payload: () => `<div class="code">${highlightCode(json(args), "json")}</div>`, result: () => `<div class="code">${esc(contentText(entry.result ?? entry.content ?? message.content))}</div>`, schema: () => `<div class="code">${esc(toolNameOf(entry))}(${esc(json(args))})</div>`, timing: () => timing ? `<div class="kv"><div class="k">Started</div><div>${esc(fmtClock(timing.startedAt))}</div><div class="k">Completed</div><div>${esc(fmtClock(timing.completedAt))}</div><div class="k">Duration</div><div>${esc(formatToolTiming(timing))}</div><div class="k">Status</div><div>${timing.isError ? "Failed" : "Completed"}</div><div class="k">Source</div><div>${timingEntryType}</div></div>` : `<div class="kv"><div class="k">Duration</div><div>timing unavailable</div><div class="k">Source</div><div>${timingEntryType}</div></div>` }; patch($("tool-pane"), (panes[state.toolPane] || panes.summary)()); $("tool-tabs").querySelectorAll("span").forEach((tab) => tab.classList.toggle("on", tab.dataset.pane === state.toolPane)); }
|
|
752
|
+
function outputForInspector(agent) { return agent.output || { status: "unavailable" }; }
|
|
753
|
+
function renderOutputPane(agent) {
|
|
754
|
+
const output = outputForInspector(agent);
|
|
755
|
+
if (output.status === "available") return typeof output.value === "string" ? `<div class="code">${esc(output.value)}</div>` : `<div class="code">${highlightCode(json(output.value), "json")}</div>`;
|
|
756
|
+
if (output.status === "pending") return '<div class="hint">Output not yet available.</div>';
|
|
757
|
+
if (output.status === "unavailable") return '<div class="hint">Output unavailable.</div>';
|
|
758
|
+
if (output.status === "truncated") return `<div class="hint">${output.kind === "failure" ? "Failure" : "Output"} is truncated (${esc(output.bytes)} bytes); the complete value is not available.</div>`;
|
|
759
|
+
return `<div class="code">${esc(`${output.status === "cancelled" ? "Cancelled" : "Failed"}: ${output.code}: ${output.message}`)}</div>`;
|
|
760
|
+
}
|
|
761
|
+
function renderSystemPane(agent) {
|
|
762
|
+
const promptAvailable = typeof agent.systemPrompt === "string" || typeof agent.prompt === "string";
|
|
763
|
+
const prompt = typeof agent.systemPrompt === "string" ? agent.systemPrompt : typeof agent.prompt === "string" ? agent.prompt : "";
|
|
764
|
+
const { skillFilters, skillResolved, extensionFilters, extensionResolved } = sessionResources(agent);
|
|
765
|
+
const attempt = agent.attemptDetails?.at(-1);
|
|
766
|
+
const attempts = (agent.attemptDetails || []).map((detail) => ({ attempt: detail.attempt, transport: detail.transport, cwd: detail.setup?.cwd, model: detail.setup?.model, tools: detail.setup?.tools, ...(detail.error ? { error: detail.error } : {}) }));
|
|
767
|
+
const environment = { role: agent.role, requestedModel: agent.requestedModel, model: agent.model, cwd: agent.cwd || attempt?.setup?.cwd || selected()?.record?.run?.cwd, mode: agent.mode, contextFiles: agent.contextFiles, worktree: agent.worktree || (agent.worktreeOwner ? { owner: agent.worktreeOwner } : undefined), structuralPath: agent.structuralPath, parentId: agent.parentId, parentBreadcrumb: agent.parentBreadcrumb, handle: agent.handle, turn: agent.turn, continuity: agent.continuity, state: agent.state, attempts: agent.attempts, attempt: attempt?.attempt, attemptDetails: attempts };
|
|
768
|
+
const panes = {
|
|
769
|
+
prompt: () => `<div class="kv"><div class="k">Tokens</div><div>est. ${estTokens(prompt)}</div></div><div class="md">${promptAvailable ? sanitizeMarkdown(marked.parse(prompt, { mangle: false, headerIds: false })) : "Prompt unavailable"}</div>`,
|
|
770
|
+
tools: () => (agent.tools || []).map((tool) => { const description = toolDescriptionOf(agent, tool); return `<div class="tool-item"><b>${esc(tool)}</b>${description ? `<span>${esc(description)}</span>` : ""}</div>`; }).join("") || '<div class="hint">No tools recorded.</div>',
|
|
771
|
+
skills: () => `<div class="sec">FILTERS</div>${nameList(skillFilters)}<div class="sec">RESOLVED</div>${resourceList(skillResolved)}`,
|
|
772
|
+
extensions: () => `<div class="sec">FILTERS</div>${nameList(extensionFilters)}<div class="sec">RESOLVED</div>${resourceList(extensionResolved)}`,
|
|
773
|
+
env: () => `<div class="code">${highlightCode(json(environment), "json")}</div>`,
|
|
774
|
+
output: () => renderOutputPane(agent),
|
|
775
|
+
};
|
|
776
|
+
patch($("sys-pane"), (panes[state.sysPane] || panes.prompt)()); $("sys-tabs").querySelectorAll("span").forEach((tab) => tab.classList.toggle("on", tab.dataset.pane === state.sysPane));
|
|
777
|
+
}
|
|
778
|
+
function renderSystemPromptPane(agent, entry) { const prompt = typeof entry?.prompt === "string" ? entry.prompt : typeof agent.systemPrompt === "string" ? agent.systemPrompt : ""; const available = typeof entry?.prompt === "string" || typeof agent.systemPrompt === "string"; patch($("sys-pane"), `<div class="kv"><div class="k">Tokens</div><div>est. ${estTokens(prompt)}</div></div><div class="md">${available ? sanitizeMarkdown(marked.parse(prompt, { mangle: false, headerIds: false })) : "System prompt unavailable"}</div>`); }
|
|
779
|
+
function renderAssistantToolCalls(entry, entries) { const toolCalls = toolCallsOf(entry); return toolCalls.length ? `<div class="assistant-tools">${toolCalls.map((call) => `<div class="assistant-tool"><span class="pill tool">${esc(call.name)}</span><div class="code">${highlightCode(json(toolArgsOf(call, entries)), "json")}</div></div>`).join("")}</div>` : ""; }
|
|
780
|
+
function renderAgentDetails(agent, root = $("insp")) { patch(root, `<div class="ins-head"><span class="pill context">AGENT</span><b class="tool-name">${esc(agent.label || agent.name)}</b><span class="ins-sub">${esc(agent.state || "")}</span></div><div class="itabs" id="sys-tabs"><span data-pane="prompt">Prompt</span><span data-pane="tools">Tools</span><span data-pane="skills">Skills</span><span data-pane="extensions">Extensions</span><span data-pane="env">Environment</span><span data-pane="output">Output</span></div><div class="ins-body"><div id="sys-pane"></div></div>`); renderSystemPane(agent); }
|
|
781
|
+
function renderInspector(detail, agent, entries, root = $("insp")) { if (!detail) { patch(root, '<div class="ins-body hint">Select an event.</div>'); return; } if (detail.kind === "tool" || detail.kind === "skill") { patch(root, `<div class="ins-head"><span class="pill ${detail.kind}">${eventLabel(detail.kind)}</span><b class="tool-name">${esc(toolNameOf(detail.entry))}</b><span class="ins-sub">${esc(detail.entry.timestamp || "")}</span></div><div class="itabs" id="tool-tabs"><span data-pane="summary">Summary</span><span data-pane="payload">Input</span><span data-pane="result">Output</span><span data-pane="schema">Schema</span><span data-pane="timing">Timing</span></div><div class="ins-body"><div id="tool-pane"></div></div>`); renderToolPane(detail, entries); return; } if (detail.kind === "system") { const prompt = typeof detail.entry.prompt === "string" ? detail.entry.prompt : agent.systemPrompt; patch(root, `<div class="ins-head"><span class="pill system">SYSTEM</span><span class="ins-sub">est. ${estTokens(prompt || "")} tok</span></div><div class="ins-body"><div id="sys-pane"></div></div>`); renderSystemPromptPane(agent, detail.entry); return; } const message = detail.entry.message || detail.entry; const body = detail.kind === "assistant" ? assistantBodyText(message.content ?? detail.entry.content) : contentText(message.content ?? detail.entry.content); const tokenRows = messageTokenRows(detail.kind, message, body); const bodyHtml = detail.kind === "assistant" ? sanitizeMarkdown(marked.parse(body)) : esc(body); const tokenHtml = tokenRows ? `<div class="kv">${tokenRows}</div>` : ""; const assistantTools = detail.kind === "assistant" ? renderAssistantToolCalls(detail.entry, entries) : ""; patch(root, `<div class="ins-head"><span class="pill ${detail.kind}">${eventLabel(detail.kind)}</span></div><div class="ins-body">${tokenHtml}<div class="${detail.kind === "assistant" ? "md" : "code"}">${bodyHtml}</div>${assistantTools}</div>`); }
|
|
692
782
|
function requestTranscript(found, agentId) {
|
|
693
783
|
if (!found || !state.socket || state.socket.readyState !== 1) return;
|
|
694
784
|
const isSubagent = found.target.kind === "subagent"; if (!agentId && !isSubagent) return; const key = isSubagent ? transcriptKey(found.publisher.id, found.record.id) : transcriptKey(found.publisher.id, found.record.run.id, agentId);
|
|
@@ -697,8 +787,8 @@
|
|
|
697
787
|
if (metadata && (metadata.status === "empty" || metadata.status === "missing" || metadata.status === "failed" || metadata.status === "oversized" || metadata.status === "disconnected")) { cacheTranscript(key, metadata.timing || [], found.record, metadata.revision, metadata.status); return; }
|
|
698
788
|
if (state.transcriptPending[key]) return;
|
|
699
789
|
if (!sourceChanged && hasCache && !state.transcriptRefresh[key]) return;
|
|
700
|
-
const requestId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`; state.transcriptPending[key] = { source: sourceToken, requestId };
|
|
701
790
|
const revision = metadata && typeof metadata.revision === "number" ? metadata.revision : undefined;
|
|
791
|
+
const requestId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`; state.transcriptPending[key] = { source: sourceToken, requestId, revision };
|
|
702
792
|
state.socket.send(isSubagent ? JSON.stringify({ type: "ui:transcript", requestId, publisherId: found.publisher.id, subagentId: found.record.id, ...(revision === undefined ? {} : { revision }) }) : JSON.stringify({ type: "ui:transcript", requestId, publisherId: found.publisher.id, runId: found.record.run.id, agentId, ...(revision === undefined ? {} : { revision }) }));
|
|
703
793
|
}
|
|
704
794
|
function renderAgent() {
|
|
@@ -710,20 +800,32 @@
|
|
|
710
800
|
$("run-crumb").textContent = isSubagent ? "SUBAGENTS" : found.record.run.workflowName; $("agent-crumb").textContent = agent.label || agent.name;
|
|
711
801
|
const entries = agentEntries(found.record, agent); const { times, start, span, hasTime } = timelineScale(entries); if (!hasTime) state.agentRange = null; const timeline = $("agent-timeline"); timeline.dataset.timelineStart = String(start); timeline.dataset.timelineSpan = String(span); timeline.dataset.timelineHasTime = String(hasTime); patch($("agent-ticks"), renderAgentTimeline(entries));
|
|
712
802
|
const transcriptRevisionKey = isSubagent ? transcriptKey(found.publisher.id, found.record.id) : transcriptKey(found.publisher.id, found.record.run.id, agent.id); const eventsSig = `${targetKey(found.target)}:${agent.id}:${state.transcriptRevision[transcriptRevisionKey] || 0}:${entries.length}:${entries.at(-1)?.timestamp || entries.at(-1)?.type || ""}:${state.selectedEvent ?? ""}:${state.timelineHighlight ?? ""}`; if (state.eventsSig !== eventsSig) { patch(events, entries.length ? entries.map((entry, index) => { const detail = entryDetails(entry, agent, entries); const t = (times[index] - start) / span * 100; const compact = compactSkillReadPreview(entry, entries); const preview = compact !== undefined ? esc(compact) : eventKind(entry) === "tool" ? toolPreviewHtml(entry, entries) : eventKind(entry) === "assistant" ? renderAssistantPreview(eventPreviewParts(entry, entries)) : esc(eventPreview(entry, entries)); return `<div class="evt${index === state.selectedEvent ? " on" : ""}${index === state.timelineHighlight ? " timeline-highlight" : ""}" data-event="${index}" data-t="${t}" data-search="${esc(eventSearchText(entry, entries))}"><div class="gutter">${esc(entry.timestamp || `#${index + 1}`)}</div><span class="pill ${detail.kind}">${eventLabel(detail.kind)}</span><div class="preview">${preview}</div></div>`; }).join("") : `<div class="hint">${esc(transcriptEmptyMessage(transcriptRevisionKey))}</div>`); events.scrollTop = eventsScroll; state.eventsSig = eventsSig; }
|
|
713
|
-
const detail = entries[state.selectedEvent || 0] ? entryDetails(entries[state.selectedEvent || 0], agent, entries) : undefined; const sig = `${targetKey(found.target)}:${agent.id}:${state.selectedEvent ?? 0}:${state.sysPane}:${state.toolPane}:${detail?.kind || ""}:${detail ? json(detail.entry) : ""}`;
|
|
714
|
-
patch($("agent-stats"), renderAgentStats(found, agent, isSubagent)); if (state.inspSig !== sig) { renderInspector(detail, agent, entries); state.inspSig = sig; }
|
|
803
|
+
const detail = entries[state.selectedEvent || 0] ? entryDetails(entries[state.selectedEvent || 0], agent, entries) : undefined; const sig = `${targetKey(found.target)}:${agent.id}:${agent.state}:${agent.attempts ?? 0}:${json(agent.model)}:${agent.requestedModel || ""}:${state.inspMode}:${state.selectedEvent ?? 0}:${state.sysPane}:${state.toolPane}:${detail?.kind || ""}:${detail ? json(detail.entry) : ""}:${json(agent.output)}`;
|
|
804
|
+
patch($("agent-stats"), renderAgentStats(found, agent, isSubagent)); if (state.inspSig !== sig) { if (state.inspMode === "agent") renderAgentDetails(agent); else renderInspector(detail, agent, entries); state.inspSig = sig; }
|
|
715
805
|
if (scroller && $("insp").contains(scroller)) scroller.scrollTop = scroll; paintBrush(timeline, ".axis .ticks", state.agentRange); $("reset-agent-range").classList.toggle("hidden", !state.agentRange); applyEventSearch();
|
|
716
806
|
}
|
|
717
807
|
function clearEventHighlight() { if (eventHighlightTimer !== undefined) clearTimeout(eventHighlightTimer); eventHighlightTimer = undefined; state.timelineHighlight = null; $("events")?.querySelectorAll(".timeline-highlight").forEach((candidate) => candidate.classList.remove("timeline-highlight")); }
|
|
718
808
|
function highlightEvent(index) { const events = $("events"); if (!Number.isInteger(index) || index < 0 || !events) return; const item = [...events.querySelectorAll("[data-event]")].find((candidate) => Number(candidate.dataset.event) === index); if (!item) return; clearEventHighlight(); state.timelineHighlight = index; applyEventSearch(); events.querySelectorAll(".timeline-highlight").forEach((candidate) => candidate.classList.remove("timeline-highlight")); void item.offsetWidth; item.classList.add("timeline-highlight"); item.scrollIntoView({ block: "nearest" }); const timer = setTimeout(() => { if (eventHighlightTimer !== timer) return; eventHighlightTimer = undefined; if (state.timelineHighlight === index) state.timelineHighlight = null; events.querySelectorAll(".timeline-highlight").forEach((candidate) => candidate.classList.remove("timeline-highlight")); applyEventSearch(); }, 1200); eventHighlightTimer = timer; }
|
|
719
809
|
function nearestTimelineEvent(root, clientX) { let nearest; for (const candidate of root.querySelectorAll("[data-event]")) { const box = candidate.getBoundingClientRect(); const distance = Math.abs(clientX - (box.left + box.width / 2)); if (!nearest || distance < nearest.distance) nearest = { candidate, distance }; } return nearest?.candidate; }
|
|
720
|
-
function scheduleRender() { if (renderFrame) return; renderFrame = requestAnimationFrame(() => { renderFrame = 0; renderSidebar(); if (document.body.dataset.view === "run") renderRun(); else renderAgent(); }); }
|
|
810
|
+
function scheduleRender() { if (renderFrame) return; renderFrame = requestAnimationFrame(() => { renderFrame = 0; sendFocus(); if (document.hidden) return; renderSidebar(); if (document.body.dataset.view === "run") renderRun(); else renderAgent(); }); }
|
|
721
811
|
function applyEventSearch() { const query = ($("q")?.value || "").toLowerCase(); const range = state.agentRange; document.querySelectorAll("#events .evt").forEach((event) => { const t = Number(event.dataset.t); const inRange = !range || (Number.isFinite(t) && t >= range.from && t <= range.to); const searchText = `${event.textContent} ${event.dataset.search || ""}`.toLowerCase(); const inQuery = !query || searchText.includes(query); const highlighted = Number(event.dataset.event) === state.timelineHighlight; event.style.display = highlighted || inRange && inQuery ? "" : "none"; }); }
|
|
722
812
|
function setView(view, mode = "push") { document.body.dataset.view = view; clearEventHighlight(); $("view-run").classList.toggle("hidden", view !== "run"); $("view-agent").classList.toggle("hidden", view !== "agent" && view !== "subagent"); if (view === "run") renderRun(); else renderAgent(); syncUrl(mode); }
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
function
|
|
726
|
-
|
|
813
|
+
let lastFocusSent;
|
|
814
|
+
// The server ships tool-timing only for the focused run, so it has to learn which run this tab shows.
|
|
815
|
+
function sendFocus(force = false) {
|
|
816
|
+
const socket = state.socket;
|
|
817
|
+
if (!socket || socket.readyState !== 1) return;
|
|
818
|
+
const target = document.hidden ? undefined : currentTarget();
|
|
819
|
+
const focus = target?.kind === "run" ? { publisherId: target.publisherId, runId: target.id } : { publisherId: null, runId: null };
|
|
820
|
+
const key = `${focus.publisherId}\t${focus.runId}`;
|
|
821
|
+
if (!force && key === lastFocusSent) return;
|
|
822
|
+
lastFocusSent = key;
|
|
823
|
+
socket.send(JSON.stringify({ type: "ui:focus", ...focus }));
|
|
824
|
+
}
|
|
825
|
+
function syncUrl(mode) { sendFocus(); const url = new URL(location.href); const target = currentTarget(); url.searchParams.set("view", document.body.dataset.view); if (target?.kind === "run") url.searchParams.set("run", state.currentRun); else url.searchParams.delete("run"); if (target?.kind === "subagent") url.searchParams.set("subagent", state.currentRun); else url.searchParams.delete("subagent"); if (document.body.dataset.view === "agent" && target?.kind === "run" && state.currentAgent) url.searchParams.set("agent", state.currentAgent); else url.searchParams.delete("agent"); const next = url.pathname + url.search; if (mode !== "pop" && next !== location.pathname + location.search) try { (mode === "replace" ? history.replaceState : history.pushState).call(history, { view: document.body.dataset.view, run: target?.kind === "run" ? state.currentRun : null, subagent: target?.kind === "subagent" ? state.currentRun : null, agent: target?.kind === "run" ? state.currentAgent : null, target }, "", next); } catch { /* Static exports run from file: or sandboxed iframes where history writes can throw; navigation state stays in memory. */ } }
|
|
826
|
+
function selectHome(mode = "replace") { const homePublisher = state.currentPub || currentTarget()?.publisherId || null; state.currentRun = null; state.currentTarget = null; state.pendingTarget = null; state.currentPub = homePublisher; state.currentAgent = null; state.agentRange = null; state.agentRangeAgent = null; state.selectedEvent = null; state.inspMode = "event"; state.eventsSig = null; closeScript(); renderSidebar(); setView("run", mode); }
|
|
827
|
+
function selectRun(ref, mode = "push") { const target = targetFromRef("run", ref); if (!target) return; setTarget(target); state.pendingTarget = null; state.currentPub = target.publisherId; state.currentAgent = selected()?.record.run.agents[0]?.id || null; state.selectedEvent = null; state.inspMode = "event"; state.eventsSig = null; renderSidebar(); setView("run", mode); }
|
|
828
|
+
function selectSubagent(ref, mode = "push") { const target = targetFromRef("subagent", ref.includes(":") ? ref : `${state.currentPub}:${ref}`); if (!target) return; setTarget(target); state.currentPub = target.publisherId; state.currentAgent = target.id; state.selectedEvent = null; state.inspMode = "event"; state.agentRange = null; state.eventsSig = null; renderSidebar(); setView("subagent", mode); }
|
|
727
829
|
function sendAction(action, extra = {}) { const found = selected(); if (!found || !state.socket || state.socket.readyState !== 1 || !found.publisher.connected) { showToast("publisher offline"); return; } const requestId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`; state.actionTargets.set(requestId, found.target); state.socket.send(JSON.stringify({ type: "ui:action", requestId, publisherId: found.publisher.id, target: found.target, action, ...extra })); showToast(`sent ${action}`); }
|
|
728
830
|
function showToast(text) { $("toast").textContent = text; $("toast").style.display = "block"; setTimeout(() => { $("toast").style.display = "none"; }, 1800); }
|
|
729
831
|
function paintBrush(root, selector, range) { let brush = root.querySelector(":scope > .brush"); if (!range) { brush?.classList.add("hidden"); return; } if (!brush) { brush = document.createElement("div"); brush.className = "brush"; patch(brush, '<span class="range-edge start"></span><span class="range-edge end"></span>'); root.appendChild(brush); } const track = root.querySelector(selector); if (!track) return; const box = track.getBoundingClientRect(), rootBox = root.getBoundingClientRect(); if (!box.width) return; const from = Math.min(range.from, range.to), to = Math.max(range.from, range.to); brush.classList.remove("hidden"); brush.style.left = `${box.left - rootBox.left + from / 100 * box.width}px`; brush.style.width = `${(to - from) / 100 * box.width}px`; const start = Number(root.dataset.timelineStart), span = Number(root.dataset.timelineSpan); const clock = (position) => root.dataset.timelineHasTime === "true" && Number.isFinite(start) && Number.isFinite(span) ? fmtClock(start + span * position / 100) : "—"; const startEdge = brush.querySelector(".range-edge.start"), endEdge = brush.querySelector(".range-edge.end"); startEdge?.replaceChildren(clock(from)); endEdge?.replaceChildren(clock(to)); if (startEdge && endEdge) endEdge.classList.toggle("stacked", brush.clientWidth < startEdge.offsetWidth + endEdge.offsetWidth + 6); }
|
|
@@ -733,40 +835,54 @@
|
|
|
733
835
|
function openScript() { const record = selected()?.record; if (!record?.snapshot?.script) return; patch($("script-full"), numbered(record.snapshot.script)); $("script-title").textContent = `${record.run.workflowName} script`; $("script-bg").classList.remove("hidden"); $("script-modal").classList.remove("hidden"); }
|
|
734
836
|
function closeScript() { $("script-bg").classList.add("hidden"); $("script-modal").classList.add("hidden"); }
|
|
735
837
|
function discardOfflineTranscripts() { const liveIds = new Set(state.publishers.map((publisher) => publisher.id)); for (const cache of [state.transcripts, state.transcriptPending, state.transcriptSources, state.transcriptRefresh, state.transcriptRevision, state.transcriptStatus]) for (const key of Object.keys(cache)) if (!liveIds.has(key.split("\t")[0])) delete cache[key]; state.transcriptCacheOrder = state.transcriptCacheOrder.filter((key) => liveIds.has(key.split("\t")[0])); }
|
|
736
|
-
function acceptState(value) { if (!value || !Array.isArray(value.publishers)) return; if (value.initial === true && hasAcceptedState) return; if (value.truncated === true && value.publishers.length === 0 && state.publishers.length > 0) return; const shouldAutoSelect = !hasAcceptedState && !state.currentRun && !new URLSearchParams(location.search).has("view"); state.publishers = sharePublishers(state.publishers, value.publishers.filter((publisher) => publisher.connected === true)); discardOfflineTranscripts(); const current = selected(); if (state.currentRun && current) { state.currentPub = current.publisher.id; if (state.pendingTarget && targetKey(state.pendingTarget) === targetKey(current.target)) state.pendingTarget = null; } const keepPendingTarget = state.pendingTarget && currentTarget() && targetKey(state.pendingTarget) === targetKey(currentTarget()); if (state.currentRun && !current && !keepPendingTarget) { selectHome("replace"); hasAcceptedState = true; } if (shouldAutoSelect && allRuns()[0]) { const first = allRuns()[0]; setTarget(first.target); state.currentPub = first.publisher.id; state.currentAgent = first.record.run.agents[0]?.id || null; hasAcceptedState = true; } else if (shouldAutoSelect && allSubagents()[0]) { const first = allSubagents()[0]; setTarget(first.target); state.currentPub = first.publisher.id; state.currentAgent = first.record.id; document.body.dataset.view = "subagent"; $("view-run").classList.add("hidden"); $("view-agent").classList.remove("hidden"); hasAcceptedState = true; } if (state.currentRun) hasAcceptedState = true; scheduleRender(); }
|
|
838
|
+
function acceptState(value) { if (!value || !Array.isArray(value.publishers)) return; if (value.initial === true && hasAcceptedState) return; if (value.truncated === true && value.publishers.length === 0 && state.publishers.length > 0) return; const shouldAutoSelect = !hasAcceptedState && !state.currentRun && !new URLSearchParams(location.search).has("view"); state.publishers = sharePublishers(state.publishers, value.publishers.filter((publisher) => publisher.connected === true).map(runsByCompletion)); discardOfflineTranscripts(); const current = selected(); if (state.currentRun && current) { state.currentPub = current.publisher.id; if (state.pendingTarget && targetKey(state.pendingTarget) === targetKey(current.target)) state.pendingTarget = null; } const keepPendingTarget = state.pendingTarget && currentTarget() && targetKey(state.pendingTarget) === targetKey(currentTarget()); if (state.currentRun && !current && !keepPendingTarget) { selectHome("replace"); hasAcceptedState = true; } if (shouldAutoSelect && allRuns()[0]) { const first = allRuns()[0]; setTarget(first.target); state.currentPub = first.publisher.id; state.currentAgent = first.record.run.agents[0]?.id || null; hasAcceptedState = true; } else if (shouldAutoSelect && allSubagents()[0]) { const first = allSubagents()[0]; setTarget(first.target); state.currentPub = first.publisher.id; state.currentAgent = first.record.id; document.body.dataset.view = "subagent"; $("view-run").classList.add("hidden"); $("view-agent").classList.remove("hidden"); hasAcceptedState = true; } if (state.currentRun) hasAcceptedState = true; scheduleRender(); }
|
|
737
839
|
function connect() {
|
|
738
840
|
const generation = ++socketGeneration;
|
|
739
841
|
const Socket = window.WebSocket;
|
|
740
842
|
const socket = new Socket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`);
|
|
741
843
|
state.socket = socket;
|
|
742
|
-
socket.addEventListener("open", () => { if (state.socket
|
|
844
|
+
socket.addEventListener("open", () => { if (state.socket !== socket || generation !== socketGeneration) return; socket.send(JSON.stringify({ type: "ui:attach" })); sendFocus(true); });
|
|
743
845
|
socket.addEventListener("message", (event) => {
|
|
744
846
|
if (state.socket !== socket || generation !== socketGeneration) return;
|
|
745
847
|
try {
|
|
746
848
|
const message = JSON.parse(event.data);
|
|
747
849
|
if (message.type === "state") acceptState(message);
|
|
748
|
-
|
|
850
|
+
// A stale reply means the transcript moved on after this request; the cached entries stay until the newer revision is fetched.
|
|
851
|
+
if (message.type === "transcript") { const isSubagent = typeof message.subagentId === "string"; const key = isSubagent ? transcriptKey(message.publisherId, message.subagentId) : transcriptKey(message.publisherId, message.runId, message.agentId); const pending = state.transcriptPending[key]; if (!pending || pending.requestId !== message.requestId) return; delete state.transcriptPending[key]; if (message.ok === false && pending.revision !== undefined && message.revision !== pending.revision) return; if (pending.source !== state.transcriptSources[key]) { if (document.body.dataset.view === "agent" || document.body.dataset.view === "subagent") scheduleRender(); return; } if (typeof message.publisherId === "string" && (isSubagent || typeof message.runId === "string" && typeof message.agentId === "string")) { const current = selected(); const currentKey = current ? current.target.kind === "subagent" ? transcriptKey(current.publisher.id, current.record.id) : transcriptKey(current.publisher.id, current.record.run.id, state.currentAgent) : ""; const status = message.ok === true ? message.status === "empty" ? "empty" : "available" : ["missing", "failed", "oversized", "disconnected"].includes(message.status) ? message.status : "failed"; if (message.ok === true || message.ok === false || message.status === "empty" || message.status === "missing" || message.status === "failed" || message.status === "oversized" || message.status === "disconnected") cacheTranscript(key, message.ok === true && Array.isArray(message.entries) ? message.entries : [], currentKey === key ? current.record : undefined, message.revision, status); } if (message.ok === false) showToast(message.error || "transcript failed"); if (document.body.dataset.view === "agent" || document.body.dataset.view === "subagent") scheduleRender(); }
|
|
749
852
|
if (message.type === "publisher:action-result" || message.type === "action-result") { const pending = typeof message.requestId === "string" ? state.actionTargets.get(message.requestId) : undefined; if (typeof message.requestId === "string") state.actionTargets.delete(message.requestId); const result = message.result; if (message.ok && result && typeof result.shareUrl === "string") { if (pending?.kind === "run") { state.shareUrls[pending.id] = result.shareUrl; scheduleRender(); } showToast("share link ready"); return; } if (message.ok && pending?.kind === "subagent" && result && typeof result.id === "string") { const target = { kind: "subagent", publisherId: pending.publisherId, id: result.id }; state.pendingTarget = target; setTarget(target); state.currentPub = pending.publisherId; state.currentAgent = result.id; state.selectedEvent = null; state.eventsSig = null; renderSidebar(); setView("subagent", "replace"); } showToast(message.ok ? "action accepted" : `action failed: ${message.error}`); }
|
|
750
853
|
} catch { showToast("invalid Trajectory update"); }
|
|
751
854
|
});
|
|
752
855
|
socket.addEventListener("close", () => {
|
|
753
856
|
if (state.socket !== socket || generation !== socketGeneration) return;
|
|
754
857
|
state.socket = null;
|
|
858
|
+
lastFocusSent = undefined;
|
|
755
859
|
state.transcriptPending = {};
|
|
756
860
|
state.actionTargets.clear();
|
|
757
861
|
socketReconnect = setTimeout(() => { socketReconnect = undefined; if (generation === socketGeneration) connect(); }, 1000);
|
|
758
862
|
});
|
|
759
863
|
}
|
|
760
864
|
$("script-close").addEventListener("click", closeScript); $("script-bg").addEventListener("click", closeScript); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeScript(); setAgentRange(null); if (document.body.dataset.view === "run") renderRun(); else renderAgent(); } });
|
|
761
|
-
document.
|
|
865
|
+
document.addEventListener("toggle", (event) => { const target = event.target; if (!(target instanceof HTMLDetailsElement) || target.dataset.workflowArgs === undefined) return; if (target.open) state.workflowArgumentsOpen.add(target.dataset.workflowArgs); else state.workflowArgumentsOpen.delete(target.dataset.workflowArgs); }, true);
|
|
866
|
+
document.querySelector(".app").addEventListener("click", (event) => { const target = event.target.closest("[data-sidebar-group], [data-sidebar-section], [data-copy-share], [data-act], [data-run], [data-subagent], [data-agent], [data-agent-details], [data-script], [data-pane], [data-toggle]"); if (!target) return; if (target.dataset.copyShare) { const input = target.previousElementSibling; navigator.clipboard?.writeText(target.dataset.copyShare).then(() => showToast("copied"), () => { input?.select(); showToast("press Ctrl+C to copy"); }); return; } if (target.dataset.sidebarGroup !== undefined || target.dataset.sidebarSection !== undefined) { const key = target.dataset.sidebarGroup ?? target.dataset.sidebarSection; if (state.sidebarCollapsed.has(key)) state.sidebarCollapsed.delete(key); else state.sidebarCollapsed.add(key); saveSidebarCollapsed(); renderSidebar(); return; } if (target.dataset.toggle) { if (document.body.dataset.view !== "run") return; const key = target.dataset.toggle === "gantt" ? "ganttCollapsed" : target.dataset.toggle === "agents" ? "agentsCollapsed" : target.dataset.toggle === "logs" ? "logsCollapsed" : ""; if (!key) return; state.runLayout[key] = !state.runLayout[key]; saveRunLayout(); renderRun(); return; } if (target.dataset.act) { if (target.dataset.act === "steer") { const input = $("subagent-steer"); const message = input?.value.trim(); if (!message) return; sendAction("steer", { payload: { message } }); if (input) input.value = ""; return; } const name = target.closest("[data-name]")?.dataset.name; sendAction(target.dataset.act, name ? { name } : {}); return; } if (target.dataset.agentDetails !== undefined) { state.inspMode = "agent"; state.inspSig = null; renderAgent(); return; } if (target.dataset.script) { openScript(); return; } if (target.dataset.pane) { const found = selected(); const agent = found ? found.target.kind === "subagent" ? subagentAgent(found.record) : found.record.run.agents.find((candidate) => candidate.id === state.currentAgent) : undefined; const entries = found && agent ? agentEntries(found.record, agent) : []; if (target.closest("#tool-tabs")) { state.toolPane = target.dataset.pane; state.inspMode = "event"; renderInspector(entries[state.selectedEvent || 0] ? entryDetails(entries[state.selectedEvent || 0], agent, entries) : undefined, agent, entries); } else { state.sysPane = target.dataset.pane; state.inspMode = "agent"; if (agent) renderSystemPane(agent); } return; } if (target.dataset.run) { selectRun(target.dataset.run); return; } if (target.dataset.subagent) { selectSubagent(target.dataset.subagent); return; } if (target.dataset.agent && document.body.dataset.view === "run") { state.currentAgent = target.dataset.agent; state.selectedEvent = null; state.inspMode = "event"; setView("agent"); } });
|
|
867
|
+
document.addEventListener("visibilitychange", () => { sendFocus(); if (!document.hidden) scheduleRender(); });
|
|
868
|
+
// Phones show one sheet at a time, so picking something moves to the sheet that shows its result.
|
|
869
|
+
const phone = matchMedia("(max-width: 900px)");
|
|
870
|
+
function showMobilePane(pane) { document.documentElement.dataset.mobilePane = pane; $("mobile-tabs").querySelectorAll("[data-mobile-pane]").forEach((button) => button.classList.toggle("on", button.dataset.mobilePane === pane)); }
|
|
871
|
+
$("mobile-tabs").addEventListener("click", (event) => { const button = event.target.closest("[data-mobile-pane]"); if (button) showMobilePane(button.dataset.mobilePane); });
|
|
872
|
+
document.addEventListener("click", (event) => {
|
|
873
|
+
if (event.target.closest("[data-theme-toggle]")) { toggleTheme(); return; }
|
|
874
|
+
if (!phone.matches) return;
|
|
875
|
+
if (event.target.closest("#sidebar [data-run], #sidebar [data-subagent], #insp [data-agent], #insp [data-run]")) showMobilePane("stage");
|
|
876
|
+
else if (event.target.closest("#events .evt, [data-agent-details]")) showMobilePane("insp");
|
|
877
|
+
});
|
|
762
878
|
$("q").addEventListener("input", applyEventSearch); $("run-crumb").addEventListener("click", () => document.body.dataset.view === "subagent" ? selectHome("push") : setView("run"));
|
|
763
|
-
$("events").addEventListener("click", (event) => { const item = event.target.closest("[data-event]"); if (!item) return; state.selectedEvent = Number(item.dataset.event); renderAgent(); });
|
|
879
|
+
$("events").addEventListener("click", (event) => { const item = event.target.closest("[data-event]"); if (!item) return; state.selectedEvent = Number(item.dataset.event); state.inspMode = "event"; renderAgent(); });
|
|
764
880
|
$("agent-timeline").addEventListener("click", (event) => { const timeline = $("agent-timeline"); if (timeline.dataset.brushMoved === "true") { timeline.dataset.brushMoved = "false"; return; } const item = event.target.closest("[data-event]") || (event.target.closest(".ticks") ? nearestTimelineEvent(timeline, event.clientX) : undefined); if (!item) return; highlightEvent(Number(item.dataset.event)); });
|
|
765
881
|
window.addEventListener("popstate", (event) => { const next = event.state || Object.fromEntries(new URL(location.href).searchParams); const view = next.view || "run"; const ref = view === "subagent" ? next.subagent || next.run : next.run; state.currentRun = ref || null; state.currentAgent = next.agent || null; state.currentTarget = next.target || targetFromRef(view === "subagent" ? "subagent" : "run", ref) || null; state.pendingTarget = null; state.eventsSig = null; setView(view, "pop"); });
|
|
766
882
|
$("reset-agent-range").addEventListener("click", () => setAgentRange(null)); bindTimeCursor($("swim"), ".axis .ticks"); bindTimeCursor($("agent-timeline"), ".axis .ticks", $("events")); bindBrush($("agent-timeline"), ".axis .ticks", setAgentRange); bindSplit($("split-left"), "--left", 180, 520, { direction: "left" }); bindSplit($("split-right"), "--right", 240, 640, { direction: "right" }); bindSplit($("split-swim"), "--swim", 72, maxSwimHeight, { axis: "y", onResize: (height) => { state.runLayout.swimHeight = height; }, onEnd: saveRunLayout });
|
|
767
883
|
function bindSplit(element, property, min, max, options = {}) { element.addEventListener("mousedown", (event) => { if (event.button !== 0) return; event.preventDefault(); element.classList.add("drag"); const axis = options.axis || "x"; const start = axis === "y" ? event.clientY : event.clientX; const size = parseFloat(getComputedStyle(document.documentElement).getPropertyValue(property)); const move = (next) => { const coordinate = axis === "y" ? next.clientY : next.clientX; const delta = axis === "y" ? coordinate - start : options.direction === "left" ? coordinate - start : start - coordinate; const limit = typeof max === "function" ? max() : max; const value = Math.round(Math.max(min, Math.min(limit, size + delta))); document.documentElement.style.setProperty(property, `${value}px`); options.onResize?.(value); }; const up = () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); element.classList.remove("drag"); options.onEnd?.(); }; window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); }); }
|
|
768
884
|
window.addEventListener("resize", clampSwimHeight);
|
|
769
|
-
|
|
885
|
+
renderRunLayout(); const query = new URLSearchParams(location.search); const initialView = query.get("view") || "run"; const initialRef = initialView === "subagent" ? query.get("subagent") || query.get("run") : query.get("run"); state.currentRun = initialRef; state.currentAgent = query.get("agent"); document.body.dataset.view = initialView; state.currentTarget = targetFromRef(initialView === "subagent" ? "subagent" : "run", initialRef) || null; if (initialView === "subagent" && !state.currentAgent) state.currentAgent = state.currentTarget?.id || null; $("view-run").classList.toggle("hidden", document.body.dataset.view !== "run"); $("view-agent").classList.toggle("hidden", document.body.dataset.view === "run"); $("view-agent").classList.toggle("hidden", document.body.dataset.view !== "agent" && document.body.dataset.view !== "subagent"); if (window.__PIEWF_STATIC__) { acceptState(window.__PIEWF_STATIC__); } else { connect(); setInterval(tickClocks, 1000); }
|
|
770
886
|
</script>
|
|
771
887
|
</body>
|
|
772
888
|
</html>
|