@marcoscale98/pi-extensible-workflows 5.14.1-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.
Files changed (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
@@ -0,0 +1,772 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Trajectory</title>
7
+ <link rel="icon" type="image/png" href="./favicon.png" />
8
+ <style>
9
+ :root {
10
+ --font: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
11
+ --bg: #141414; --bg-2: #1b1b1b; --bg-3: #222; --bg-hover: #2a2a2a; --bg-sel: #2a3140;
12
+ --line: #2c2c2c; --text: #e8e8e8; --dim: #8d8d8d; --mute: #5c5c5c; --accent: #6ea0ff;
13
+ --ok: #3ecf8e; --warn: #e6c35c; --err: #e25d5d; --left: 300px; --right: 360px; --swim: 220px;
14
+ --side: #101010; --insp: #161616;
15
+ }
16
+ html[data-theme="tty"] {
17
+ --font: ui-monospace, "IBM Plex Mono", "SF Mono", Menlo, Consolas, monospace;
18
+ --bg: #1e1e2e; --bg-2: #181825; --bg-3: #313244; --bg-hover: #313244; --bg-sel: #45475a;
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
+ *::-webkit-scrollbar { width: 10px; height: 10px; }
30
+ *::-webkit-scrollbar-track { background: var(--bg-2); }
31
+ *::-webkit-scrollbar-thumb { background: var(--mute); border: 2px solid var(--bg-2); border-radius: 6px; }
32
+ *::-webkit-scrollbar-thumb:hover { background: var(--dim); }
33
+ :root, html[data-theme="tty"], html[data-theme="harness"] { color-scheme: dark; }
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; }
38
+ button, input { font: inherit; color: inherit; }
39
+ button { cursor: pointer; }
40
+ .app { height: 100%; display: flex; min-width: 0; }
41
+ .sidebar { width: var(--left); flex: none; background: var(--side); border-right: 1px solid var(--line); display: flex; flex-direction: column; padding: 12px 10px; overflow: auto; min-width: 180px; }
42
+ .splitbar { width: 5px; flex: none; cursor: col-resize; background: var(--line); }
43
+ .splitbar.row { width: auto; height: 5px; padding: 0; cursor: row-resize; }
44
+ .splitbar:hover, .splitbar.drag, .splitbar.row:hover, .splitbar.row.drag { background: var(--accent); }
45
+ .brand { font-size: 11px; letter-spacing: .12em; color: var(--dim); font-weight: 600; padding: 4px 8px 12px; display: flex; align-items: center; gap: 6px; }
46
+ .brand .tab { border: 1px solid var(--line); background: var(--bg-3); color: var(--dim); padding: 2px 5px; font-size: 9px; }
47
+ .brand .tab.on { color: var(--accent); border-color: var(--accent); }
48
+ .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: 6px; }
50
+ .folder:hover { background: var(--bg-hover); }
51
+ .folder .marker { width: 10px; color: var(--dim); }
52
+ .folder .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
53
+ .folder .count { color: var(--dim); font: 11px ui-monospace, Menlo, Consolas, monospace; }
54
+ .group-children { margin-left: 8px; }
55
+ .pub { border-radius: 8px; padding: 4px 6px; }
56
+ .pub.on { background: #1c1c1c; }
57
+ .pub .row { display: flex; align-items: center; gap: 8px; }
58
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--mute); }
59
+ .dot.live { background: var(--ok); box-shadow: 0 0 0 3px #3ecf8e22; }
60
+ .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(--warn); font-weight: 600; }
62
+ .children { margin: 0 0 0 8px; padding: 2px 0 0 8px; border-left: 1px solid #333; }
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: 6px; font-size: 12px; }
64
+ .run-item.on { background: #1c2433; color: var(--text); }
65
+ .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: 6px; padding: 6px 8px; }
67
+ .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
+ .g-run.ok { color: var(--ok); }
69
+ .g-run.wait { color: var(--warn); }
70
+ .g-run.fail { color: var(--err); }
71
+ .g-run.queued, .g-run.paused { color: var(--mute); }
72
+ .g-run.spin { color: var(--accent); }
73
+ .g-run.spin::before { content: "⠋"; animation: braille .8s linear infinite; }
74
+ .g-run.budget { color: var(--warn); }
75
+ @keyframes braille {
76
+ 0% { content: "⠋"; } 12% { content: "⠙"; } 25% { content: "⠹"; } 37% { content: "⠸"; }
77
+ 50% { content: "⠼"; } 62% { content: "⠴"; } 75% { content: "⠦"; } 87% { content: "⠧"; } 100% { content: "⠇"; }
78
+ }
79
+ .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 6px; border-radius: 3px; background: #3a3214; color: var(--warn); }
81
+ .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
+ .stage { flex: 1; min-width: 0; display: flex; flex-direction: column; }
83
+ .top { display: flex; align-items: center; gap: 14px; padding: 8px 14px; border-bottom: 1px solid var(--line); }
84
+ .crumb { color: var(--dim); font-size: 12px; }
85
+ .crumb a { color: var(--accent); text-decoration: none; cursor: pointer; }
86
+ .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
+ .hidden { display: none !important; }
88
+ .toolbar { display: flex; align-items: center; gap: 6px; padding: 8px 14px; border-bottom: 1px solid var(--line); }
89
+ .seg button { background: none; border: 0; color: var(--dim); padding: 4px 8px; border-radius: 4px; }
90
+ .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: 6px; padding: 4px 8px; width: 180px; }
92
+ .body { flex: 1; min-height: 0; display: flex; }
93
+ .inspector { width: var(--right); flex: none; border-left: 1px solid var(--line); background: var(--insp); overflow: hidden; min-width: 220px; display: flex; flex-direction: column; }
94
+ .inspector .ins-head, .inspector .itabs { flex: none; }
95
+ .inspector .ins-body { flex: 1; min-height: 0; overflow: auto; }
96
+ #tool-pane, #sys-pane { overflow: visible; }
97
+ .inspector .code:not(.script) { flex: 1; max-height: none; min-height: 0; }
98
+ @media (max-width: 900px) {
99
+ .app { flex-direction: column; overflow: auto; }
100
+ .sidebar, .inspector { width: 100% !important; min-width: 0; max-height: 28vh; }
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; }
114
+ .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
+ .range-edge.start { left: 3px; }
116
+ .range-edge.end { right: 3px; text-align: right; }
117
+ .range-edge.end.stacked { top: -17px; }
118
+ .timeline { position: relative; }
119
+ .axis { display: grid; grid-template-columns: 100px 1fr; gap: 8px; margin-bottom: 6px; font: 10px ui-monospace, Menlo, Consolas, monospace; color: var(--mute); }
120
+ .swim .axis { position: sticky; top: 0; z-index: 4; background: var(--bg-2); }
121
+ .axis .ticks { display: flex; justify-content: space-between; position: relative; }
122
+ .timeline .axis { grid-template-columns: 44px 1fr; gap: 8px; margin-bottom: 2px; }
123
+ .now { position: absolute; width: 1px; background: #e25d5d; z-index: 2; pointer-events: none; }
124
+ .swim > .now { top: 22px; bottom: 8px; z-index: 5; }
125
+ .timeline > .now { top: 18px; bottom: 2px; }
126
+ .now span { position: absolute; top: -14px; left: 4px; color: #e25d5d; background: var(--bg-2); padding: 1px 3px; white-space: nowrap; font: 10px ui-monospace, Menlo, Consolas, monospace; }
127
+ .lane { display: grid; grid-template-columns: 16px 76px 1fr; gap: 8px; align-items: center; height: 18px; margin: 3px 0; cursor: pointer; border-radius: 3px; }
128
+ .lane:hover { background: #ffffff08; }
129
+ .lane.child { padding-left: 12px; grid-template-columns: 16px 64px 1fr; }
130
+ .lane.crumb { cursor: default; opacity: .85; }
131
+ .lane.crumb:hover { background: none; }
132
+ .lane.crumb .lab { text-align: left; color: #9aa; font-style: italic; }
133
+ .bar.crumb { background: #ffffff18; height: 4px; top: 3px; }
134
+ .crumbband { font-size: 11px; color: #9aa; padding: 4px 12px 2px 28px; font-family: ui-monospace, Menlo, Consolas, monospace; font-style: italic; }
135
+ .lane .lab { color: var(--mute); font-size: 11px; text-align: right; overflow: hidden; white-space: nowrap; }
136
+ .track { position: relative; height: 10px; }
137
+ .bar { position: absolute; top: 0; height: 10px; border-radius: 2px; }
138
+ .bar.tool { top: 5px; height: 5px; opacity: .9; }
139
+ .bar.ok { background: #2f6f55; }
140
+ .bar.spin { background: #6ea0ff; }
141
+ .bar.fail { background: var(--err); }
142
+ .bar.wait { background: var(--warn); }
143
+ .bar.paused { background: #555; }
144
+ .split { flex: 1; min-height: 0; display: flex; }
145
+ .list { flex: 1; overflow: auto; padding: 8px 0; }
146
+ .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: 8px 12px; border: 0; background: none; color: inherit; text-align: left; }
148
+ .workflow-head:hover, .workflow-head.on { background: var(--bg-sel); }
149
+ .workflow-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
150
+ .workflow-meta { color: var(--dim); font: 11px ui-monospace, Menlo, Consolas, monospace; white-space: nowrap; }
151
+ .agent-grid { min-width: 900px; padding: 0 12px 8px 28px; }
152
+ .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
+ .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, .agent-grid-row.on { background: var(--bg-hover); }
156
+ .agent-grid-row .agent-cell { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
157
+ .agent-grid-row .agent-cell:first-child { color: var(--text); font-family: ui-monospace, Menlo, Consolas, monospace; }
158
+ .agent-grid-scope { padding: 6px 8px 2px; color: var(--dim); font: 11px ui-monospace, Menlo, Consolas, monospace; font-style: italic; }
159
+ .agent-grid-row .num { color: var(--dim); text-align: right; font-family: ui-monospace, Menlo, Consolas, monospace; }
160
+ .agent-grid .phaseband { padding-left: 8px; }
161
+ .logs { padding: 0 12px 12px 28px; }
162
+ .logs-list { max-height: 240px; overflow: auto; }
163
+ .log-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 10px; padding: 5px 8px; border-top: 1px solid var(--line); font: 11px ui-monospace, Menlo, Consolas, monospace; }
164
+ .log-time { color: var(--mute); white-space: nowrap; }
165
+ .log-message { min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
166
+ .row { display: grid; grid-template-columns: 16px 1fr; gap: 8px; align-items: center; padding: 3px 12px; cursor: pointer; }
167
+ .row.child { padding-left: 28px; }
168
+ .row:hover { background: var(--bg-hover); }
169
+ .row.on { background: var(--bg-sel); }
170
+ .tree { color: var(--dim); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
171
+ .tree .nm { color: var(--text); }
172
+ .note { color: var(--err); font-size: 11px; }
173
+ .phaseband { font-size: 11px; color: #5ec8d6; padding: 8px 12px 2px; letter-spacing: .04em; }
174
+ .timeline { padding: 6px 12px 8px; border-bottom: 1px solid var(--line); background: linear-gradient(#1d1d1d 1px, transparent 1px) 0 0 / 100% 16px, linear-gradient(90deg, #1d1d1d 1px, transparent 1px) 0 0 / 18px 100%, #151515; position: relative; user-select: none; }
175
+ .timeline > .brush { top: 18px; bottom: 2px; }
176
+ .tl-lane { display: grid; grid-template-columns: 44px 1fr; gap: 8px; align-items: center; height: 16px; }
177
+ .tl-lane .lab { font-size: 10px; color: var(--mute); }
178
+ .ticks { position: relative; height: 12px; }
179
+ .tick { position: absolute; top: 2px; width: 8px; height: 8px; border-radius: 1px; cursor: pointer; }
180
+ .tick.in { background: #3ecf8e; }
181
+ .tick.model { background: #7b6ad8; }
182
+ .tick.tl { background: #c47a20; }
183
+ .tick.err { background: var(--err); }
184
+ .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; }
186
+ .evt > .pill { justify-self: end; }
187
+ .evt:hover { background: var(--bg-hover); }
188
+ .evt.on { background: #243044; border-left-color: var(--accent); }
189
+ @keyframes timeline-event-highlight { from { background: var(--accent); } to { background: transparent; } }
190
+ .evt.timeline-highlight { animation: timeline-event-highlight 1.2s ease-out; }
191
+ .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: 700; letter-spacing: .04em; padding: 2px 6px; border-radius: 3px; text-align: center; }
193
+ .pill.system { background: #3a3a3a; color: #ddd; }
194
+ .pill.user { background: #2a4a7a; color: #cfe0ff; }
195
+ .pill.context { background: #1d5a3a; color: #b8f5d4; }
196
+ .pill.assistant { background: #3d2f6a; color: #ddd0ff; }
197
+ .pill.tool { background: #5a3a10; color: #ffd08a; }
198
+ .pill.skill { background: #164e63; color: #a5f3fc; }
199
+ .pill.issue { background: #5a1818; color: #ffb0b0; }
200
+ .preview { color: var(--dim); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
201
+ .preview .pill.tool { margin-right: 4px; }
202
+ .preview .preview-tool { font-size: 9px; padding: 1px 4px; }
203
+ .preview .tool-preview-name, .preview .tool-value { color: var(--text); }
204
+ .preview .tool-key, .preview .tool-gap, .preview .tool-timing { color: var(--dim); }
205
+ .preview .tool-value { font-weight: 400; }
206
+ .preview b { color: var(--text); font-weight: 400; }
207
+ .ins-head { padding: 10px 12px 6px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--line); }
208
+ .ins-sub { color: var(--dim); font-size: 12px; }
209
+ .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 12px 0; border-bottom: 1px solid var(--line); }
211
+ .itabs span { color: var(--dim); padding-bottom: 8px; border-bottom: 2px solid transparent; cursor: pointer; font-size: 12px; }
212
+ .itabs span.on { color: var(--accent); border-bottom-color: var(--accent); }
213
+ .ins-body { padding: 12px; font-size: 12px; }
214
+ .md { line-height: 1.5; }
215
+ .md h1, .md h2, .md h3 { margin: .7em 0 .3em; font-size: 13px; }
216
+ .md p { margin: 0 0 .7em; }
217
+ .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; }
219
+ .md a { color: var(--accent); }
220
+ .md pre.code { max-height: none; white-space: pre-wrap; }
221
+ .kv { display: grid; grid-template-columns: 90px 1fr; gap: 6px 10px; margin-bottom: 12px; }
222
+ .kv .k { color: var(--dim); }
223
+ .code { background: #101010; border: 1px solid var(--line); border-radius: 6px; padding: 8px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; white-space: pre-wrap; color: #c0caf5; overflow: auto; max-height: 240px; }
224
+ .assistant-tools { display: grid; gap: 8px; margin-top: 12px; }
225
+ .assistant-tool { display: grid; gap: 4px; }
226
+ .code .kw { color: #7aa2f7; }
227
+ .code .str { color: #9ece6a; }
228
+ .code.script { max-height: 4.8em; cursor: pointer; }
229
+ .code.script:hover { border-color: #556; }
230
+ .ln { display: grid; grid-template-columns: 44px 1fr; }
231
+ .ln .no { color: var(--mute); text-align: right; padding-right: 12px; user-select: none; }
232
+ .modal-bg { position: fixed; inset: 0; background: #000c; z-index: 40; }
233
+ .modal { position: fixed; inset: 32px; background: #0e0e0e; border: 1px solid #333; border-radius: 8px; z-index: 41; display: flex; flex-direction: column; }
234
+ .modal-bar { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--line); }
235
+ .modal-bar .sp { flex: 1; }
236
+ .modal-bar button { background: var(--bg-3); border: 0; border-radius: 6px; padding: 4px 10px; }
237
+ .modal .code { flex: 1; max-height: none; border: 0; border-radius: 0; }
238
+ .tool-item { padding: 7px 8px; border-bottom: 1px solid var(--line); }
239
+ .tool-item b { font-family: ui-monospace, Menlo, Consolas, monospace; }
240
+ .tool-item span { color: var(--dim); display: block; }
241
+ .actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
242
+ .actions button { border: 0; border-radius: 6px; padding: 6px 10px; }
243
+ .actions button:disabled { opacity: .45; cursor: not-allowed; }
244
+ .btn-ok { background: #1d5a3a; color: #b8f5d4; }
245
+ .btn-no { background: #5a1818; color: #ffb0b0; }
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; }
248
+ .hint { color: var(--mute); font-size: 11px; padding: 0 12px 8px; }
249
+ .acct { width: 100%; border-collapse: collapse; font: 11px ui-monospace, Menlo, Consolas, monospace; }
250
+ .acct th { text-align: left; color: var(--mute); font-weight: 500; padding: 0 6px 6px; }
251
+ .acct td { padding: 4px 6px; border-top: 1px solid var(--line); }
252
+ .acct .num { text-align: right; color: var(--dim); }
253
+ .acct tr { cursor: pointer; }
254
+ .acct tr:hover td { background: var(--bg-hover); }
255
+ .pending { border: 1px solid #5a4b14; background: #1a1608; border-radius: 6px; padding: 8px; margin-bottom: 10px; }
256
+ .pending.issue { border-color: #5a1818; background: #1a1010; }
257
+ .pending .ttl { font-weight: 600; margin-bottom: 4px; display: flex; gap: 6px; align-items: center; }
258
+ .sec { color: var(--mute); font-size: 10px; letter-spacing: .08em; margin: 14px 0 8px; }
259
+ .section-toggle { display: block; width: 100%; padding: 0; border: 0; background: none; font: inherit; text-align: left; cursor: pointer; }
260
+ #toggle-gantt { padding: 0 14px; }
261
+ .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-2); color: var(--text); border: 1px solid var(--line); border-radius: 3px; padding: 3px 6px; font: inherit; font-size: 11px; }
263
+ html[data-theme="paper"] .code { background: #1b1f24; color: #e6edf3; }
264
+ html[data-theme="paper"] .swim, html[data-theme="paper"] .timeline {
265
+ background: linear-gradient(#e6e8eb 1px, transparent 1px) 0 0 / 100% 18px,
266
+ linear-gradient(90deg, #e6e8eb 1px, transparent 1px) 0 0 / 24px 100%, #f4f5f7;
267
+ }
268
+ html[data-theme="paper"] .pub.on { background: #e2e8f4; }
269
+ html[data-theme="paper"] .run-item.on { background: #dbe7ff; }
270
+ html[data-theme="paper"] .evt.on { background: #e8eef9; }
271
+ html[data-theme="paper"] .pending { background: #fff7e6; border-color: #f0d9a8; }
272
+ html[data-theme="paper"] .pending.issue { background: #fdecec; border-color: #f0b4b4; }
273
+ html[data-theme="paper"] .pill.system { background: #e8eaed; color: #3c424a; }
274
+ html[data-theme="paper"] .pill.user { background: #dbe7ff; color: #1e3a8a; }
275
+ html[data-theme="paper"] .pill.context { background: #d9f0e3; color: #166534; }
276
+ html[data-theme="paper"] .pill.assistant { background: #ece4ff; color: #4c1d95; }
277
+ html[data-theme="paper"] .pill.tool { background: #ffedd5; color: #9a3412; }
278
+ html[data-theme="paper"] .pill.skill { background: #cffafe; color: #155e75; }
279
+ html[data-theme="paper"] .pill.issue { background: #fee2e2; color: #991b1b; }
280
+ html[data-theme="paper"] .btn-ok { background: #d9f0e3; color: #166534; }
281
+ html[data-theme="paper"] .btn-no { background: #fee2e2; color: #991b1b; }
282
+ html[data-theme="paper"] .btn-alt { background: #dbe7ff; color: #1e3a8a; }
283
+ html[data-theme="paper"] .toast { background: #fff; color: #1c1917; }
284
+ html[data-theme="tty"] .pub.on, html[data-theme="tty"] .run-item.on { background: #313244; }
285
+ html[data-theme="tty"] .evt.on { background: #313244; }
286
+ html[data-theme="tty"] .pending { background: #181825; border-color: #f9e2af; }
287
+ html[data-theme="tty"] .pending.issue { background: #181825; border-color: #f38ba8; }
288
+ html[data-theme="tty"] .pill.system { background: #313244; color: #cdd6f4; }
289
+ html[data-theme="tty"] .pill.user { background: #313244; color: #89b4fa; }
290
+ html[data-theme="tty"] .pill.context { background: #313244; color: #a6e3a1; }
291
+ html[data-theme="tty"] .pill.assistant { background: #313244; color: #cba6f7; }
292
+ html[data-theme="tty"] .pill.tool { background: #313244; color: #fab387; }
293
+ html[data-theme="tty"] .pill.skill { background: #313244; color: #89dceb; }
294
+ html[data-theme="tty"] .pill.issue { background: #313244; color: #f38ba8; }
295
+ html[data-theme="tty"] .btn-ok { background: #313244; color: #a6e3a1; }
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;
308
+ }
309
+ </style>
310
+ </head>
311
+ <body data-view="run">
312
+ <div class="app">
313
+ <aside class="sidebar" id="sidebar"></aside>
314
+ <div class="splitbar" id="split-left"></div>
315
+ <div class="stage" id="view-run">
316
+ <div class="top">
317
+ <div class="crumb"><span id="canvas-pub">—</span> / <span id="canvas-run">—</span></div>
318
+ <div class="run-stats" id="run-stats"></div>
319
+ </div>
320
+ <button type="button" class="sec section-toggle" id="toggle-gantt" data-toggle="gantt" aria-expanded="true">▾ GANTT</button>
321
+ <div class="swim" id="swim"><div id="swim-content"></div><div class="now hidden"><span></span></div></div>
322
+ <div class="splitbar row" id="split-swim"></div>
323
+ <div class="split"><div class="list" id="list"></div></div>
324
+ </div>
325
+ <div class="stage hidden" id="view-agent">
326
+ <div class="top">
327
+ <div class="crumb"><a data-go="run" id="run-crumb">—</a> / <span id="agent-crumb">—</span></div>
328
+ <div class="run-stats" id="agent-stats"></div>
329
+ </div>
330
+ <div class="toolbar">
331
+ <b>EVENTS</b><button type="button" class="btn-alt hidden" id="reset-agent-range">Clear time range</button><input class="search" id="q" placeholder="Search events" />
332
+ </div>
333
+ <div class="timeline" id="agent-timeline"><div id="agent-ticks"></div><div class="now hidden"><span></span></div></div>
334
+ <div class="body"><div class="events" id="events"></div></div>
335
+ </div>
336
+ <div class="splitbar" id="split-right"></div>
337
+ <aside class="inspector" id="insp"></aside>
338
+ </div>
339
+ <div class="toast" id="toast"></div>
340
+ <div class="modal-bg hidden" id="script-bg"></div>
341
+ <div class="modal hidden" id="script-modal">
342
+ <div class="modal-bar"><b id="script-title">script</b><span class="sp"></span><button type="button" id="script-close">close</button></div>
343
+ <div class="code" id="script-full"></div>
344
+ </div>
345
+ <script src="./marked.min.js"></script>
346
+ <script src="./morphdom.min.js"></script>
347
+ <script>
348
+ const defaultRunLayout = () => ({ swimHeight: 220, ganttCollapsed: false, agentsCollapsed: false, logsCollapsed: false });
349
+ const maxSwimHeight = () => Math.max(72, window.innerHeight * 0.7);
350
+ function loadRunLayout() {
351
+ const fallback = defaultRunLayout();
352
+ try {
353
+ const raw = localStorage.getItem("traj-run-layout");
354
+ if (!raw) return fallback;
355
+ const value = JSON.parse(raw);
356
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fallback;
357
+ return {
358
+ swimHeight: Number.isFinite(value.swimHeight) && value.swimHeight >= 72 ? value.swimHeight : fallback.swimHeight,
359
+ ganttCollapsed: typeof value.ganttCollapsed === "boolean" ? value.ganttCollapsed : fallback.ganttCollapsed,
360
+ agentsCollapsed: typeof value.agentsCollapsed === "boolean" ? value.agentsCollapsed : fallback.agentsCollapsed,
361
+ logsCollapsed: typeof value.logsCollapsed === "boolean" ? value.logsCollapsed : fallback.logsCollapsed,
362
+ };
363
+ } catch {
364
+ return fallback;
365
+ }
366
+ }
367
+ function saveRunLayout() { try { localStorage.setItem("traj-run-layout", JSON.stringify(state.runLayout)); } catch { /* Preferences are optional. */ } }
368
+ function loadSidebarCollapsed() {
369
+ try {
370
+ const raw = localStorage.getItem("traj-sidebar-collapsed");
371
+ if (!raw) return new Set();
372
+ const value = JSON.parse(raw);
373
+ return Array.isArray(value) && value.every((key) => typeof key === "string") ? new Set(value) : new Set();
374
+ } catch {
375
+ return new Set();
376
+ }
377
+ }
378
+ 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() };
380
+ let hasAcceptedState = false;
381
+ let socketGeneration = 0;
382
+ let socketReconnect;
383
+ let renderFrame = 0;
384
+ let eventHighlightTimer;
385
+ const $ = (id) => document.getElementById(id);
386
+ function patch(root, html) { const target = document.createElement(root.nodeName); target.insertAdjacentHTML("afterbegin", html); const focused = document.activeElement; const focusedTabIndex = focused?.tabIndex; morphdom(root, target, { childrenOnly: true, getNodeKey: (node) => node.id || node.dataset?.event || node.dataset?.agent || node.dataset?.subagent || undefined }); if (focused?.isConnected && focused !== document.activeElement) { if (Number.isFinite(focusedTabIndex) && focusedTabIndex >= 0) focused.tabIndex = focusedTabIndex; focused.focus(); } }
387
+ const esc = (value) => String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
388
+
389
+ const json = (value) => { try { return JSON.stringify(value, null, 2); } catch { return String(value); } };
390
+ const fmtRuntime = (ms) => { const s = Math.max(0, Math.floor(ms / 1000)); if (s < 60) return `${s}s`; const m = Math.floor(s / 60), r = s % 60; if (m < 60) return `${m}m${r ? ` ${r}s` : ""}`; const h = Math.floor(m / 60), rm = m % 60; return `${h}h${rm ? ` ${rm}m` : ""}`; };
391
+ const fmtCost = (cost) => `$${Number(cost || 0).toFixed(Number(cost || 0) < .01 ? 3 : 2)}`;
392
+ const fmtTokens = (tokens) => Number(tokens || 0) < 1000 ? `${Number(tokens || 0)}t` : `${(Number(tokens || 0) / 1000).toFixed(Number(tokens || 0) / 1000 < 10 ? 1 : 0)}kt`;
393
+ const fmtClock = (stamp) => stamp ? new Date(stamp).toLocaleTimeString() : "—";
394
+ const targetRef = (target) => `${target.publisherId}:${target.id}`;
395
+ const targetKey = (target) => `${target.kind}:${targetRef(target)}`;
396
+ const targetFromRef = (kind, ref) => { if (typeof ref !== "string") return undefined; const separator = ref.indexOf(":"); if (separator < 1 || separator === ref.length - 1) return undefined; return { kind, publisherId: ref.slice(0, separator), id: ref.slice(separator + 1) }; };
397
+ const runKey = (publisher, run) => `${publisher.id}:${run.run.id}`;
398
+ const subagentKey = (publisher, subagent) => `${publisher.id}:${subagent.id}`;
399
+ const transcriptKey = (publisherId, targetId, agentId) => agentId === undefined ? `${publisherId}\tsubagent\t${targetId}` : `${publisherId}\t${targetId}\t${agentId}`;
400
+ const sameValue = (left, right) => { try { return JSON.stringify(left) === JSON.stringify(right); } catch { return false; } };
401
+ 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
+ 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]; const keys = Object.keys(shared); return keys.length === Object.keys(old).length && keys.every((key) => shared[key] === old[key]) ? old : shared; }
403
+ 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
+ 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
+ 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; }
406
+ function sharePublishers(previous, incoming) { const old = Array.isArray(previous) ? previous : []; for (const publisher of incoming) { const prior = old.find((candidate) => candidate?.id === publisher?.id); if (prior && prior.generation !== publisher.generation) invalidatePublisherTranscripts(publisher.id); } const shared = incoming.map((publisher) => sharePublisher(old.find((candidate) => candidate?.id === publisher?.id), publisher)); return old.length === shared.length && shared.every((publisher, index) => publisher === old[index]) ? old : shared; }
407
+ const livePublishers = () => state.publishers.filter((publisher) => publisher.connected === true);
408
+ const sidebarGroupKey = (publisher) => typeof publisher.cwd === "string" && publisher.cwd ? publisher.cwd : "";
409
+ const sidebarGroupLabel = (key) => key ? key.split(/[\\/]/).filter(Boolean).at(-1) || "unknown" : "unknown";
410
+ function sidebarGroups(publishers) {
411
+ const groups = new Map();
412
+ for (const publisher of publishers) {
413
+ const key = sidebarGroupKey(publisher);
414
+ const group = groups.get(key) || { key, label: sidebarGroupLabel(key), publishers: [] };
415
+ group.publishers.push(publisher);
416
+ groups.set(key, group);
417
+ }
418
+ return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label) || left.key.localeCompare(right.key));
419
+ }
420
+ const allRuns = () => livePublishers().flatMap((publisher) => (publisher.runs || []).map((run) => ({ publisher, record: run, target: { kind: "run", publisherId: publisher.id, id: run.run.id } })));
421
+ const allSubagents = () => livePublishers().flatMap((publisher) => (publisher.subagents || []).map((subagent) => ({ publisher, record: subagent, target: { kind: "subagent", publisherId: publisher.id, id: subagent.id } })));
422
+ const allTargets = () => [...allRuns(), ...allSubagents()];
423
+ const currentTarget = () => state.currentTarget || (state.currentRun ? targetFromRef("run", state.currentRun) : undefined);
424
+ const setTarget = (target) => { state.currentTarget = target; state.currentRun = target ? targetRef(target) : null; };
425
+ const selected = () => { const target = currentTarget(); return target ? allTargets().find((candidate) => targetKey(candidate.target) === targetKey(target)) : undefined; };
426
+ const runState = (record) => record.run.state;
427
+ const stateClass = (value) => ({ running: "spin", queued: "queued", pausing: "spin", paused: "paused", awaiting_input: "wait", completed: "ok", failed: "fail", stopped: "fail", interrupted: "wait", budget_exhausted: "budget" }[value] || "queued");
428
+ const glyph = (value) => { const c = stateClass(value); return c === "spin" ? '<span class="g-run spin"></span>' : c === "ok" ? '<span class="g-run ok">✓</span>' : c === "wait" ? '<span class="g-run wait">?</span>' : c === "fail" ? '<span class="g-run fail">✗</span>' : c === "paused" ? '<span class="g-run paused">⏸</span>' : c === "budget" ? '<span class="g-run budget">!</span>' : '<span class="g-run queued">○</span>'; };
429
+ const accounting = (agent) => agent.accounting || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
430
+ const runCost = (record) => record.run.usage?.costUsd ?? record.run.agents.reduce((sum, agent) => sum + accounting(agent).cost, 0);
431
+ const runTokens = (record) => record.run.usage?.tokens ?? record.run.agents.reduce((sum, agent) => sum + accounting(agent).input + accounting(agent).output, 0);
432
+ 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
+ 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)); };
434
+ const attention = (record) => (record.awaiting || []).length + record.run.agents.filter((agent) => agent.state === "failed").length;
435
+ const duration = (record) => Math.max(0, endOf(record) - startOf(record).getTime());
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)}%`; }); const axisEnd = swim.querySelector('[data-clock-axis="end"]'); if (axisEnd?.firstChild) axisEnd.firstChild.nodeValue = fmtClock(now); }
438
+ const highlight = (source) => esc(source).replace(/(&quot;.*?&quot;)/g, '<span class="str">$1</span>').replace(/\b(await|return|const|async|agent|parallel|pipeline|phase|checkpoint)\b/g, '<span class="kw">$1</span>');
439
+ const numbered = (source) => String(source || "").split("\n").map((line, index) => `<div class="ln"><span class="no">${index + 1}</span><span>${highlight(line)}</span></div>`).join("");
440
+ 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
+ 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
+ const toolCallsOf = (entry) => { const content = entry?.message?.content; return Array.isArray(content) ? content.filter((part) => part && part.type === "toolCall") : []; };
443
+ const estTokens = (text) => Math.ceil(String(text || "").length / 4);
444
+ const messageTokenRows = (kind, message, body) => {
445
+ if (kind === "user") return "";
446
+ const usage = message.usage;
447
+ if (!usage) return `<div class="k">Tokens</div><div>est. ${estTokens(body)} tok</div>`;
448
+ const rows = [`<div class="k">Input</div><div>${esc(usage.input ?? 0)} tok</div>`, `<div class="k">Output</div><div>${esc(usage.output ?? 0)} tok</div>`];
449
+ if (typeof usage.reasoning === "number") rows.push(`<div class="k">Reasoning</div><div>${esc(usage.reasoning)} tok</div>`);
450
+ if (typeof usage.cacheRead === "number" && usage.cacheRead !== 0) rows.push(`<div class="k">Cache read</div><div>${esc(usage.cacheRead)} tok</div>`);
451
+ if (typeof usage.cacheWrite === "number" && usage.cacheWrite !== 0) rows.push(`<div class="k">Cache write</div><div>${esc(usage.cacheWrite)} tok</div>`);
452
+ return rows.join("");
453
+ };
454
+ const markdownAllowedTags = new Set(["a", "blockquote", "br", "code", "del", "em", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "img", "input", "li", "ol", "p", "pre", "strong", "table", "tbody", "td", "th", "thead", "tr", "ul"]);
455
+ const markdownAllowedAttributes = new Set(["alt", "checked", "class", "disabled", "href", "rel", "src", "target", "title", "type"]);
456
+ function sanitizeMarkdown(html) {
457
+ const parsed = new DOMParser().parseFromString(String(html), "text/html");
458
+ for (const element of parsed.body.querySelectorAll("*")) {
459
+ const tag = element.tagName.toLowerCase();
460
+ if (!markdownAllowedTags.has(tag)) { element.remove(); continue; }
461
+ for (const attribute of [...element.attributes]) {
462
+ const name = attribute.name.toLowerCase();
463
+ const value = attribute.value;
464
+ const safeClass = name !== "class" || (tag === "code" && /^(?:language|lang)-[a-z0-9_-]+(?:\s+(?:language|lang)-[a-z0-9_-]+)*$/i.test(value));
465
+ 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
+ }
467
+ }
468
+ return parsed.body.innerHTML;
469
+ }
470
+ const isDisplayableTranscriptEntry = (entry) => entry.type !== "session" && entry.type !== "session_info" && entry.type !== "model_change" && entry.type !== "thinking_level_change";
471
+ const timingEntryType = "pi-workflows:tool-timing"; const isTimingEntry = (entry) => entry?.type === "custom" && entry?.customType === timingEntryType; const timingData = (entry) => { const data = entry?.data; return data && typeof data.toolCallId === "string" && typeof data.toolName === "string" && Number.isFinite(data.startedAt) && Number.isFinite(data.completedAt) && Number.isFinite(data.durationMs) && typeof data.isError === "boolean" ? data : undefined; }; const toolCallPartOf = (entry) => { const content = entry?.message?.content; return Array.isArray(content) ? content.find((part) => part && part.type === "toolCall") : undefined; }; const toolCallIdOf = (entry) => entry.toolCallId || entry.message?.toolCallId || toolCallPartOf(entry)?.id; const isToolResult = (entry) => entry.type === "tool_result" || entry.type === "message" && entry.message?.role === "toolResult"; const eventKind = (entry) => { if (entry.type === "agent_outcome") return entry.outcome?.kind === "failure" ? "issue" : "context"; if (entry.type === "system_prompt" || entry.type === "session" || entry.type === "session_info") return "system"; if (entry.type === "message") { const role = entry.message?.role; return role === "toolResult" ? "tool" : role === "user" ? "user" : "assistant"; } return entry.type === "tool_result" ? "tool" : "context"; }; const toolNameOf = (entry) => entry.toolName || entry.message?.toolName || entry.message?.name || toolCallPartOf(entry)?.name || entry.name || "tool"; const toolTimingOf = (entry) => entry?._toolTiming || timingData(entry); const toolResultIsError = (entry) => entry.isError === true || entry.message?.isError === true || toolTimingOf(entry)?.isError === true; const formatToolTiming = (timing) => timing ? `${timing.durationMs}ms${timing.isError ? " (failed)" : ""}` : "timing unavailable";
472
+ const transcriptSource = (record, agent) => { const found = selected(); const target = found?.target; const key = found && found.record === record ? target.kind === "subagent" ? transcriptKey(found.publisher.id, target.id) : transcriptKey(found.publisher.id, record.run.id, agent.id) : ""; if (key && Object.prototype.hasOwnProperty.call(state.transcripts, key)) return state.transcripts[key]; const value = target?.kind === "subagent" ? record.transcript : record.transcripts?.[agent.id]; return Array.isArray(value) ? value : value?.timing || []; };
473
+ const agentTimingRecords = (record, agent) => { const source = transcriptSource(record, agent); return source.flatMap((entry) => { const timing = isTimingEntry(entry) ? timingData(entry) : undefined; return timing ? [{ name: timing.toolName, timing }] : []; }); }; const agentEntries = (record, agent) => { const source = transcriptSource(record, agent); const timings = new Map(agentTimingRecords(record, agent).map(({ timing }) => [timing.toolCallId, timing])); const entries = source.filter((entry) => isDisplayableTranscriptEntry(entry) && !isTimingEntry(entry)).map((entry) => { const timing = isToolResult(entry) ? timings.get(toolCallIdOf(entry)) : undefined; return timing ? { ...entry, _toolTiming: timing } : entry; }); if (agent.systemPrompt) entries.unshift({ type: "system_prompt", prompt: agent.systemPrompt }); if (agent.outcome) entries.push({ type: "agent_outcome", outcome: agent.outcome, message: { role: "outcome", content: json(agent.outcome.value) }, timestamp: Number.isFinite(agent.finishedAt) ? new Date(agent.finishedAt).toISOString() : undefined }); return entries; };
474
+ const subagentTimingRecords = (subagent) => { const publisher = state.publishers.find((candidate) => (candidate.subagents || []).includes(subagent)); const key = publisher ? transcriptKey(publisher.id, subagent.id) : ""; const metadata = key && Object.prototype.hasOwnProperty.call(state.transcripts, key) ? state.transcripts[key] : subagent.transcript; const source = Array.isArray(metadata) ? metadata : metadata?.timing || []; return source.flatMap((entry) => { const timing = isTimingEntry(entry) ? timingData(entry) : undefined; return timing ? [{ name: timing.toolName, timing }] : []; }); };
475
+ const toolTimingSummary = (timings) => { const limit = 8; const visible = timings.slice(0, limit).map(({ name, timing }) => `${name} ${formatToolTiming(timing)}`); return visible.length ? `${visible.join(", ")}${timings.length > limit ? ` +${timings.length - limit} more` : ""}` : "—"; };
476
+ const agentTimingMapCache = new WeakMap(); const agentTimingMap = (record) => { const cached = agentTimingMapCache.get(record); if (cached) return cached; const map = new Map(record.run.agents.map((agent) => [agent.id, agentTimingRecords(record, agent)])); agentTimingMapCache.set(record, map); return map; };
477
+ function cacheTranscript(key, entries, record, revision, status = entries.length ? "available" : "empty") { if (status === "disconnected") { invalidatePublisherTranscripts(key.split("\t")[0] || ""); return; } state.transcripts[key] = entries; state.transcriptStatus[key] = status; state.transcriptCacheOrder = state.transcriptCacheOrder.filter((value) => value !== key); state.transcriptCacheOrder.push(key); while (state.transcriptCacheOrder.length > 64) { const evicted = state.transcriptCacheOrder.shift(); if (evicted) { delete state.transcripts[evicted]; delete state.transcriptStatus[evicted]; } } delete state.transcriptRefresh[key]; state.transcriptRevision[key] = typeof revision === "number" ? revision : (state.transcriptRevision[key] || 0) + 1; if (record) agentTimingMapCache.delete(record); }
478
+ function invalidatePublisherTranscripts(publisherId) { for (const cache of [state.transcripts, state.transcriptPending, state.transcriptSources, state.transcriptRefresh, state.transcriptRevision, state.transcriptStatus]) for (const key of Object.keys(cache)) if (key.split("\t")[0] === publisherId) delete cache[key]; state.transcriptCacheOrder = state.transcriptCacheOrder.filter((key) => key.split("\t")[0] !== publisherId); }
479
+ 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
+ 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
+ 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 || [] }; };
483
+ 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
+ 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
+ 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>';
486
+ const eventTime = (entry) => { const raw = toolTimingOf(entry)?.startedAt ?? entry.timestamp ?? entry.message?.timestamp; const value = typeof raw === "string" ? Date.parse(raw) : Number(raw); return Number.isFinite(value) ? value : undefined; };
487
+ function timelineScale(entries) {
488
+ const finiteTimes = entries.flatMap((entry) => [eventTime(entry), toolTimingOf(entry)?.completedAt]).filter((time) => time !== undefined);
489
+ const hasTime = finiteTimes.length > 0;
490
+ const start = hasTime ? Math.min(...finiteTimes) : 0;
491
+ const end = hasTime ? Math.max(...finiteTimes) : start + 1;
492
+ let previous = start;
493
+ const times = entries.map((entry) => {
494
+ const time = eventTime(entry);
495
+ if (time !== undefined) previous = time;
496
+ return previous;
497
+ });
498
+ return { times, start, span: Math.max(1, end - start), hasTime };
499
+ }
500
+ function renderAgentTimeline(entries) {
501
+ const { times, start, span, hasTime } = timelineScale(entries);
502
+ const lanes = { input: [], model: [], tools: [] };
503
+ entries.forEach((entry, index) => {
504
+ const timing = toolTimingOf(entry); const left = ((timing?.startedAt ?? times[index]) - start) / span * 100;
505
+ const kind = eventKind(entry);
506
+ const lane = kind === "tool" ? "tools" : kind === "assistant" ? "model" : "input";
507
+ const cls = toolResultIsError(entry) || kind === "issue" ? "err" : kind === "tool" ? "tl" : kind === "assistant" ? "model" : "in";
508
+ lanes[lane].push(`<i class="tick ${cls}" data-event="${index}" style="left:${left}%;${timing ? `width:${Math.max(0.5, (timing.completedAt - timing.startedAt) / span * 100)}%` : ""}" title="${esc(kind)}${timing ? ` · ${esc(formatToolTiming(timing))}` : ""}"></i>`);
509
+ });
510
+ const lane = (name, key) => `<div class="tl-lane"><div class="lab">${name}</div><div class="ticks">${lanes[key].join("")}</div></div>`;
511
+ const clock = (stamp) => hasTime ? fmtClock(stamp) : "—";
512
+ const middle = hasTime ? `+${fmtRuntime(span / 2)}` : "—";
513
+ const axis = `<div class="axis"><div></div><div class="ticks"><span>${clock(start)}</span><span>${middle}</span><span>${clock(start + span)}</span></div></div>`;
514
+ return `${axis}${lane("Input", "input")}${lane("Model", "model")}${lane("Tools", "tools")}`;
515
+ }
516
+ const compactSkillReadPreview = (entry, entries = []) => {
517
+ const content = entry.message?.content;
518
+ const toolCalls = Array.isArray(content) ? content.filter((part) => part && part.type === "toolCall") : [];
519
+ if (toolNameOf(entry) !== "read" || (!isToolResult(entry) && toolCalls.length !== 1)) return undefined;
520
+ const args = toolArgsOf(entry, entries);
521
+ if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
522
+ const rawPath = args.file_path ?? args.path;
523
+ if (typeof rawPath !== "string") return undefined;
524
+ const pathParts = rawPath.replaceAll("\\", "/").split("/").filter(Boolean);
525
+ if (pathParts.at(-1) !== "SKILL.md") return undefined;
526
+ const startLine = args.offset ?? 1;
527
+ const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
528
+ const lineRange = args.offset === undefined && args.limit === undefined ? "" : `:${startLine}${endLine ? `-${endLine}` : ""}`;
529
+ // Trajectory has no session cwd, so a root-level SKILL.md keeps this fallback label.
530
+ return `[skill] ${pathParts.at(-2) || "SKILL.md"}${lineRange}`;
531
+ };
532
+
533
+ const isPlainObject = (value) => {
534
+ if (!value || Object.prototype.toString.call(value) !== "[object Object]") return false;
535
+ const prototype = Object.getPrototypeOf(value);
536
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
537
+ };
538
+ const toolPreviewValue = (value) => {
539
+ let full;
540
+ if (typeof value === "string") full = value;
541
+ else if (value && typeof value === "object") {
542
+ try { full = JSON.stringify(value); } catch { full = String(value); }
543
+ } else full = String(value);
544
+ full = String(full ?? value);
545
+ const row = full.replace(/[\r\n\t]/g, " ");
546
+ return { full, row: row.length > 80 ? `${row.slice(0, 80)}…` : row };
547
+ };
548
+ const toolPreviewData = (entry, entries = []) => {
549
+ const args = toolArgsOf(entry, entries);
550
+ const fields = isPlainObject(args) ? Object.keys(args).map((key) => ({ key, value: toolPreviewValue(args[key]) })) : undefined;
551
+ return { name: toolNameOf(entry), fields, fallback: fields ? undefined : toolPreviewValue(args), timing: toolTimingOf(entry) };
552
+ };
553
+ const toolPreviewText = (entry, entries = []) => {
554
+ const { name, fields, fallback, timing } = toolPreviewData(entry, entries);
555
+ const args = fields ? fields.map(({ key, value }) => `${key} ${value.row}`).join(" · ") : `${fallback.row}`;
556
+ return `${name}${fields ? (args ? ` ${args}` : "") : `: ${args}`}${timing ? ` · ${formatToolTiming(timing)}` : ""}`;
557
+ };
558
+ const toolPreviewHtml = (entry, entries = []) => {
559
+ const { name, fields, fallback, timing } = toolPreviewData(entry, entries);
560
+ const timingHtml = timing ? `<span class="tool-timing"> · ${esc(formatToolTiming(timing))}</span>` : "";
561
+ if (!fields) return `<span class="tool-preview-name">${esc(name)}</span><span class="tool-gap">: </span><span class="tool-value" title="${esc(fallback.full)}">${esc(fallback.row)}</span>${timingHtml}`;
562
+ const fieldsHtml = fields.map(({ key, value }, index) => `${index ? '<span class="tool-gap"> · </span>' : '<span class="tool-gap"> </span>'}<span class="tool-pair"><span class="tool-key">${esc(key)}</span><span class="tool-gap"> </span><span class="tool-value" title="${esc(value.full)}">${esc(value.row)}</span></span>`).join("");
563
+ return `<span class="tool-preview-name">${esc(name)}</span>${fieldsHtml}${timingHtml}`;
564
+ };
565
+ const eventPreview = (entry, entries = []) => {
566
+ const compact = compactSkillReadPreview(entry, entries);
567
+ if (compact !== undefined) return compact;
568
+ if (entry.type === "system_prompt") return "Initial System Prompt";
569
+ if (entry.type === "agent_outcome") return entry.outcome?.kind === "failure" ? "Agent failure" : "Agent result";
570
+ if (eventKind(entry) === "tool") return toolPreviewText(entry, entries);
571
+ if (entry.type === "message") return contentText(entry.message?.content);
572
+ if (entry.type === "session" || entry.type === "session_info") return "Session started";
573
+ return entry.type;
574
+ };
575
+ const eventPreviewParts = (entry, entries = []) => { const compact = compactSkillReadPreview(entry, entries); if (compact !== undefined) return { text: compact, names: [], overflow: 0 }; const toolCalls = toolCallsOf(entry); if (eventKind(entry) === "assistant") { const text = Array.isArray(entry.message?.content) ? entry.message.content.filter((part) => part && part.type === "text").map((part) => part.text || "").join(" ") : ""; const names = toolCalls.map((call) => call.name); if (text || names.length) return { text, names, overflow: Math.max(0, names.length - 3) }; } return { text: eventPreview(entry, entries), names: [], overflow: 0 }; };
576
+ 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
+ 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
+ const eventLabel = (kind) => kind === "toolResult" ? "TOOL" : kind === "context" ? "RESULT" : kind === "issue" ? "FAILURE" : String(kind || "EVENT").toUpperCase();
579
+ function renderThemeButtons() {
580
+ const enabled = state.publishers.some((publisher) => publisher.themes === true);
581
+ const themes = enabled ? ["harness", "tty", "paper"] : [];
582
+ if (enabled && !["harness", "tty", "paper"].includes(document.documentElement.dataset.theme || "")) {
583
+ try { document.documentElement.dataset.theme = localStorage.getItem("traj-theme") || "tty"; } catch { document.documentElement.dataset.theme = "tty"; }
584
+ }
585
+ patch($("theme-buttons"), themes.map((theme) => `<button class="tab theme" data-theme="${theme}">${theme.toUpperCase()}</button>`).join(""));
586
+ $("theme-buttons").classList.toggle("hidden", !enabled);
587
+ $("theme-buttons").querySelectorAll("[data-theme]").forEach((button) => button.addEventListener("click", () => {
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));
594
+ }
595
+ const subagentAccounting = (subagent) => subagent.progress?.accounting || subagent.attempt?.accounting || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
596
+ const subagentCost = (subagent) => subagentAccounting(subagent).cost;
597
+ const subagentTokens = (subagent) => subagentAccounting(subagent).input + subagentAccounting(subagent).output;
598
+ const subagentStartOf = (subagent) => Number.isFinite(subagent.startedAt) ? subagent.startedAt : Date.now();
599
+ const subagentEndOf = (subagent) => subagent.state === "running" ? Date.now() : Number.isFinite(subagent.finishedAt) ? subagent.finishedAt : subagentStartOf(subagent);
600
+ const subagentRuntime = (subagent) => Math.max(0, subagentEndOf(subagent) - subagentStartOf(subagent));
601
+ const subagentAttention = (subagent) => subagent.state === "failed" ? 1 : 0;
602
+ const subagentModel = (subagent) => subagent.model ? `${subagent.model.provider || "?"}/${subagent.model.model || "?"}` : "—";
603
+ const subagentLabel = (subagent) => subagent.label || subagent.id.slice(0, 8);
604
+ function renderSidebar() {
605
+ const groups = sidebarGroups(livePublishers());
606
+ const html = groups.map((group) => {
607
+ const groupAttention = group.publishers.reduce((total, publisher) => total + (publisher.runs || []).reduce((sum, record) => sum + attention(record), 0) + (publisher.subagents || []).reduce((sum, subagent) => sum + subagentAttention(subagent), 0), 0);
608
+ const collapsed = state.sidebarCollapsed.has(group.key);
609
+ const sessions = group.publishers.map((publisher) => {
610
+ const publisherAttention = (publisher.runs || []).reduce((total, record) => total + attention(record), 0) + (publisher.subagents || []).reduce((total, subagent) => total + subagentAttention(subagent), 0);
611
+ const runs = (publisher.runs || []).map((record) => {
612
+ const ref = runKey(publisher, record);
613
+ const pending = record.awaiting?.length || attention(record);
614
+ 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("");
616
+ const subagentSectionKey = `subagents:${publisher.id}`;
617
+ const subagentsCollapsed = state.sidebarCollapsed.has(subagentSectionKey);
618
+ const subagents = (publisher.subagents || []).map((subagent) => {
619
+ const ref = subagentKey(publisher, subagent);
620
+ const target = { kind: "subagent", publisherId: publisher.id, id: subagent.id };
621
+ const pending = subagentAttention(subagent);
622
+ const telemetry = `${subagent.state} · ${fmtCost(subagentCost(subagent))} · ${fmtRuntime(subagentRuntime(subagent))}`;
623
+ 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("");
625
+ 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
+ }).join("");
627
+ 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
+ }).join("");
629
+ patch($("sidebar"), `<div class="brand">TRAJECTORY <span id="theme-buttons"></span></div>${html}`);
630
+ renderThemeButtons();
631
+ }
632
+ function renderGantt(record, timingsByAgent) {
633
+ const agents = record.run.agents; const start = startOf(record).getTime(), end = endOf(record), span = Math.max(1, end - start); const lanes = []; let previous = "";
634
+ for (const agent of agents) {
635
+ const crumb = agent.parentBreadcrumb || "";
636
+ if (crumb && crumb !== previous) { const group = agents.filter((candidate) => (candidate.parentBreadcrumb || "") === crumb); const left = Math.min(...group.map((candidate) => ((candidate.startedAt || start) - start) / span * 100)); const right = Math.max(...group.map((candidate) => ((candidate.startedAt || start) + (candidate.durationMs || 0) - start) / span * 100)); lanes.push(`<div class="lane crumb"><span></span><div class="lab">${esc(crumb)}</div><div class="track"><i class="bar crumb" style="left:${Math.max(0, left)}%;width:${Math.max(1, right - left)}%"></i></div></div>`); }
637
+ previous = crumb;
638
+ const timings = timingsByAgent.get(agent.id) || []; const left = agent.startedAt ? (agent.startedAt - start) / span * 100 : 0; const width = agent.durationMs ? agent.durationMs / span * 100 : agent.state === "running" ? Math.max(2, (Date.now() - (agent.startedAt || start)) / span * 100) : 0;
639
+ const toolBars = timings.map(({ name, timing }) => { const toolLeft = Math.max(0, (timing.startedAt - start) / span * 100); const toolWidth = Math.max(0.5, (timing.completedAt - timing.startedAt) / span * 100); return `<i class="bar tool ${timing.isError ? "fail" : "ok"}" style="left:${toolLeft}%;width:${toolWidth}%" title="${esc(`${name} · ${formatToolTiming(timing)}`)}"></i>`; }).join("");
640
+ lanes.push(`<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="left:${Math.max(0, left)}%;width:${Math.max(0, width)}%"${agent.state === "running" ? ` data-clock-bar-start="${agent.startedAt || start}" data-clock-bar-min="2"` : ""}></i>${toolBars}</div></div>`);
641
+ }
642
+ return `<div class="axis"><div></div><div class="ticks"><span>${fmtClock(start)}</span><span>+${fmtRuntime(span / 2)}</span><span data-clock-axis="end">${record.run.state === "running" ? fmtClock(Date.now()) : "end"}</span></div></div>${lanes.join("")}`;
643
+ }
644
+ function renderSubagentGantt(publisher) {
645
+ const subagents = publisher.subagents || []; if (!subagents.length) return '<div class="hint">No subagents recorded.</div>';
646
+ const start = Math.min(...subagents.map((subagent) => subagentStartOf(subagent))); const end = Math.max(start + 1, ...subagents.map((subagent) => subagentEndOf(subagent))); const span = Math.max(1, end - start);
647
+ const lanes = subagents.map((subagent) => {
648
+ const left = Math.max(0, (subagentStartOf(subagent) - start) / span * 100); const width = Math.max(0.5, (subagentEndOf(subagent) - subagentStartOf(subagent)) / span * 100);
649
+ const toolBars = subagentTimingRecords(subagent).map(({ name, timing }) => { const toolLeft = Math.max(0, (timing.startedAt - start) / span * 100); const toolWidth = Math.max(0.5, (timing.completedAt - timing.startedAt) / span * 100); return `<i class="bar tool ${timing.isError ? "fail" : "ok"}" style="left:${toolLeft}%;width:${toolWidth}%" title="${esc(`${name} · ${formatToolTiming(timing)}`)}"></i>`; }).join("");
650
+ return `<div class="lane" data-subagent="${esc(subagent.id)}">${glyph(subagent.state)}<div class="lab">${esc(subagentLabel(subagent))}</div><div class="track"><i class="bar ${stateClass(subagent.state)}" style="left:${left}%;width:${width}%"${subagent.state === "running" ? ` data-clock-bar-start="${subagentStartOf(subagent)}" data-clock-bar-min="0.5"` : ""}></i>${toolBars}</div></div>`;
651
+ }).join("");
652
+ return `<div class="axis"><div></div><div class="ticks"><span>${fmtClock(start)}</span><span>+${fmtRuntime(span / 2)}</span><span data-clock-axis="end">${fmtClock(end)}</span></div></div>${lanes}`;
653
+ }
654
+ function phases(record) { const allHistory = record.run.phaseHistory || []; const omitted = allHistory.find((phase) => phase?.type === "trajectory:truncated"); const history = allHistory.filter((phase) => phase?.type !== "trajectory:truncated"); const withOmitted = (groups) => omitted ? [...groups, { name: `${omitted.omitted || 0} phase records omitted from live state`, agents: [] }] : groups; if (history.length) { const length = record.run.agents.length; const first = Math.max(0, Math.min(length, history[0].afterAgent)); const groups = first > 0 ? [{ name: "unphased", agents: record.run.agents.slice(0, first).map((agen) => agen.id) }] : []; history.forEach((phase, index) => { const from = Math.max(0, Math.min(length, phase.afterAgent)); const to = Math.max(from, Math.min(length, history[index + 1]?.afterAgent ?? length)); groups.push({ name: phase.phase, agents: record.run.agents.slice(from, to).map((agent) => agent.id) }); }); return withOmitted(groups); } return withOmitted(record.run.agents.length ? [{ name: record.run.phase || "workflow", agents: record.run.agents.map((agent) => agent.id) }] : []); }
655
+ function agentGridGroups(agents) { const groups = new Map(); for (const agent of agents) { const key = JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); const group = groups.get(key) || { agents: [] }; group.agents.push(agent); groups.set(key, group); } return [...groups.values()].map((group) => { const structural = group.agents[0]?.structuralPath ?? []; const breadcrumbs = [...new Set(group.agents.map((agent) => agent.parentBreadcrumb).filter((value) => Boolean(value)))]; return { agents: group.agents, label: [...(structural.length ? [structural.join(" > ")] : []), ...(breadcrumbs.length === 1 ? breadcrumbs : breadcrumbs.length ? [breadcrumbs.join(" | ")] : [])].join(" > ") || "Agents" }; }); }
656
+ const agentModel = (agent) => agent.model ? `${agent.model.provider || "?"}/${agent.model.model || "?"}` : "—";
657
+ function agentDepth(agent, byId) { let depth = 0; const seen = new Set([agent.id]); for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId) { if (seen.has(parent)) break; seen.add(parent); depth += 1; } return depth; }
658
+ function renderAgentGrid(record, timingsByAgent) {
659
+ const allAgents = record.run.agents;
660
+ const byId = new Map(allAgents.map((agent) => [agent.id, agent]));
661
+ const grouped = phases(record);
662
+ const body = grouped.map((phase) => {
663
+ const phaseAgents = phase.agents.flatMap((id) => { const agent = byId.get(id); return agent ? [agent] : []; });
664
+ const groups = agentGridGroups(phaseAgents);
665
+ const showScopes = groups.length > 1 || groups.some(({ label }) => label !== "Agents");
666
+ return `<div class="phaseband">PHASE ${esc(phase.name)}</div>${groups.map((group) => `${showScopes ? `<div class="agent-grid-scope">${esc(group.label)}</div>` : ""}${group.agents.map((agent) => { const depth = agentDepth(agent, byId); const label = agent.label || agent.name; const settled = depth ? "•" : "▾"; const indent = (showScopes ? 12 : 0) + depth * 12; const toolSummary = toolTimingSummary(timingsByAgent.get(agent.id) || []); return `<div class="agent-grid-row${agent.id === state.currentAgent ? " on" : ""}" data-agent="${esc(agent.id)}"><div class="agent-cell" style="padding-left:${indent}px">${settled} ${esc(label)}</div><div class="agent-cell">${esc(agentModel(agent))}</div><div class="agent-cell">${esc(agent.role || "agent")}</div><div class="agent-cell" title="${esc(toolSummary)}">${esc(toolSummary)}</div><div class="agent-cell num">${fmtRuntime(agent.durationMs || 0)}</div><div class="agent-cell num">${fmtTokens(accounting(agent).input + accounting(agent).output)}</div><div class="agent-cell num">${fmtCost(accounting(agent).cost)}</div></div>`; }).join("")}`).join("")}`;
667
+ }).join("");
668
+ return `<div class="agent-grid"><div class="agent-grid-head"><span>AGENT</span><span>MODEL</span><span>ROLE</span><span>TOOL TIMING</span><span>DURATION</span><span>TOKENS</span><span>COST</span></div>${body}</div>`;
669
+ }
670
+ 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
+ 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)}">${fmtRuntime(duration(record))}</span></span></button>${renderAgentSection(record, timingsByAgent)}${renderLogs(record)}</section>`; }
673
+ 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] : [] }; }
675
+ function renderAgentStats(found, agent, isSubagent) {
676
+ const offline = !found.publisher.connected; const stateText = agent.state || "—"; const value = isSubagent ? subagentAccounting(found.record) : accounting(agent);
677
+ 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
+ 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
+ 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>' : ""}`;
681
+ }
682
+ 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">${highlight(record.snapshot.script)}</div>` : ""; 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)}">${fmtRuntime(duration(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}<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
+ 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
+ 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
+ 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; patch($("swim-content"), renderGantt(record, timingsByAgent)); $("swim").dataset.timelineStart = String(startOf(record).getTime()); $("swim").dataset.timelineSpan = String(Math.max(1, endOf(record) - startOf(record).getTime())); $("swim").dataset.timelineHasTime = "true"; $("swim").dataset.timelineRunning = String(record.run.state === "running"); patch($("list"), renderList(record, timingsByAgent)); renderRunLayout(); patch($("insp"), renderDossier(publisher, record)); const nextDossierBody = $("insp").querySelector(".ins-body"); if (nextDossierBody && dossierScroll) nextDossierBody.scrollTop = dossierScroll; patch($("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)}">${fmtRuntime(duration(record))}</span>`); }
687
+ 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">${highlight(json(args))}</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)); }
689
+ function renderSystemPane(agent) { const prompt = agent.systemPrompt || agent.prompt || ""; const { skillFilters, skillResolved, extensionFilters, extensionResolved } = sessionResources(agent); const panes = { prompt: () => `<div class="kv"><div class="k">Tokens</div><div>est. ${estTokens(prompt)}</div></div><div class="md">${prompt ? sanitizeMarkdown(marked.parse(prompt, { mangle: false, headerIds: false })) : "System prompt unavailable"}</div>`, 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>', skills: () => `<div class="sec">FILTERS</div>${nameList(skillFilters)}<div class="sec">RESOLVED</div>${resourceList(skillResolved)}`, extensions: () => `<div class="sec">FILTERS</div>${nameList(extensionFilters)}<div class="sec">RESOLVED</div>${resourceList(extensionResolved)}`, env: () => `<div class="code">${esc(json({ role: agent.role, state: agent.state, mode: agent.mode, model: agent.model, requestedModel: agent.requestedModel, attempts: agent.attempts, worktree: agent.worktree, structuralPath: agent.structuralPath, worktreeOwner: agent.worktreeOwner }))}</div>` }; patch($("sys-pane"), (panes[state.sysPane] || panes.prompt)()); $("sys-tabs").querySelectorAll("span").forEach((tab) => tab.classList.toggle("on", tab.dataset.pane === state.sysPane)); }
690
+ 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">${highlight(json(toolArgsOf(call, entries)))}</div></div>`).join("")}</div>` : ""; }
691
+ 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") { patch(root, `<div class="ins-head"><span class="pill system">SYSTEM</span><span class="ins-sub">est. ${estTokens(agent.systemPrompt || agent.prompt || detail.entry.prompt || "")} tok</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></div><div class="ins-body"><div id="sys-pane"></div></div>`); renderSystemPane(agent); 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
+ function requestTranscript(found, agentId) {
693
+ if (!found || !state.socket || state.socket.readyState !== 1) return;
694
+ 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);
695
+ const bulkValue = isSubagent ? found.record.transcript : found.record.transcripts?.[agentId]; const bulk = Array.isArray(bulkValue) ? bulkValue : null; const metadata = bulkValue && typeof bulkValue === "object" && !Array.isArray(bulkValue) ? bulkValue : null; const sourceToken = metadata ? `${metadata.revision}:${metadata.status}` : bulk; const sourceChanged = state.transcriptSources[key] !== sourceToken; if (sourceChanged) { state.transcriptSources[key] = sourceToken; state.transcriptRefresh[key] = true; }
696
+ const hasFullTranscript = Array.isArray(bulk) && bulk.some((entry) => entry && entry.type !== "custom"); const hasCache = Object.prototype.hasOwnProperty.call(state.transcripts, key); if (hasFullTranscript && !hasCache) { cacheTranscript(key, bulk, found.record); return; }
697
+ 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
+ if (state.transcriptPending[key]) return;
699
+ if (!sourceChanged && hasCache && !state.transcriptRefresh[key]) return;
700
+ const requestId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`; state.transcriptPending[key] = { source: sourceToken, requestId };
701
+ const revision = metadata && typeof metadata.revision === "number" ? metadata.revision : undefined;
702
+ 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
+ }
704
+ function renderAgent() {
705
+ const scroller = $("insp")?.querySelector(".code:not(.script)"); const scroll = scroller ? scroller.scrollTop : 0; const events = $("events"); const eventsScroll = events.scrollTop; const found = selected();
706
+ if (!found) { renderHome(); return; }
707
+ const isSubagent = found.target.kind === "subagent"; const agent = isSubagent ? subagentAgent(found.record) : found.record.run.agents.find((candidate) => candidate.id === state.currentAgent) || found.record.run.agents[0];
708
+ if (!agent) { renderHome(); return; }
709
+ if (state.currentAgent !== agent.id) clearEventHighlight(); state.currentAgent = agent.id; requestTranscript(found, agent.id); if (state.agentRangeAgent !== agent.id) { state.agentRangeAgent = agent.id; state.agentRange = null; }
710
+ $("run-crumb").textContent = isSubagent ? "SUBAGENTS" : found.record.run.workflowName; $("agent-crumb").textContent = agent.label || agent.name;
711
+ 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
+ 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; }
715
+ if (scroller && $("insp").contains(scroller)) scroller.scrollTop = scroll; paintBrush(timeline, ".axis .ticks", state.agentRange); $("reset-agent-range").classList.toggle("hidden", !state.agentRange); applyEventSearch();
716
+ }
717
+ function clearEventHighlight() { if (eventHighlightTimer !== undefined) clearTimeout(eventHighlightTimer); eventHighlightTimer = undefined; state.timelineHighlight = null; $("events")?.querySelectorAll(".timeline-highlight").forEach((candidate) => candidate.classList.remove("timeline-highlight")); }
718
+ 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
+ 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(); }); }
721
+ 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
+ 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
+ function syncUrl(mode) { 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. */ } }
724
+ 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.eventsSig = null; closeScript(); renderSidebar(); setView("run", mode); }
725
+ 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.eventsSig = null; renderSidebar(); setView("run", mode); }
726
+ 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.agentRange = null; state.eventsSig = null; renderSidebar(); setView("subagent", mode); }
727
+ 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
+ function showToast(text) { $("toast").textContent = text; $("toast").style.display = "block"; setTimeout(() => { $("toast").style.display = "none"; }, 1800); }
729
+ 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); }
730
+ function setAgentRange(range) { state.agentRange = range; paintBrush($("agent-timeline"), ".axis .ticks", range); $("reset-agent-range").classList.toggle("hidden", !range); applyEventSearch(); }
731
+ function bindBrush(root, selector, callback) { if (!root || root.dataset.brushBound) return; root.dataset.brushBound = "1"; let drag; root.addEventListener("mousedown", (event) => { root.dataset.brushMoved = "false"; const track = root.querySelector(selector); if (event.button !== 0 || root.dataset.timelineHasTime !== "true" || !track) return; const box = track.getBoundingClientRect(); if (!box.width || event.clientX < box.left || event.clientX > box.right) return; drag = { x: event.clientX, box, moved: false }; }); window.addEventListener("mousemove", (event) => { if (!drag) return; if (Math.abs(event.clientX - drag.x) < 5) return; drag.moved = true; root.dataset.brushMoved = "true"; const from = Math.max(0, Math.min(100, (drag.x - drag.box.left) / drag.box.width * 100)); const to = Math.max(0, Math.min(100, (event.clientX - drag.box.left) / drag.box.width * 100)); const range = { from: Math.min(from, to), to: Math.max(from, to) }; callback(range); }); window.addEventListener("mouseup", () => { if (drag?.moved) root.dataset.brushMoved = "true"; drag = undefined; }); root.addEventListener("dblclick", () => callback(null)); }
732
+ function bindTimeCursor(root, selector, hoverRoot = root) { if (!root || root.dataset.cursorBound) return; root.dataset.cursorBound = "1"; const cursor = root.querySelector(":scope > .now"); if (!cursor) return; const syncCursor = () => { cursor.style.transform = `translateY(${root.scrollTop}px)`; }; const hide = () => cursor.classList.add("hidden"); const positionCursor = (position, requireTime = false) => { if (requireTime && root.dataset.timelineHasTime !== "true") { hide(); return; } const track = root.querySelector(selector); if (!track || !Number.isFinite(position)) { hide(); return; } const box = track.getBoundingClientRect(), rootBox = root.getBoundingClientRect(); if (!box.width) { hide(); return; } syncCursor(); cursor.style.left = `${box.left - rootBox.left + position / 100 * box.width}px`; const start = Number(root.dataset.timelineStart), span = Number(root.dataset.timelineSpan); const label = cursor.querySelector("span"); if (label) label.textContent = root.dataset.timelineHasTime === "true" && Number.isFinite(start) && Number.isFinite(span) ? fmtClock(start + span * position / 100) : "—"; cursor.classList.remove("hidden"); }; root.addEventListener("scroll", syncCursor); syncCursor(); root.addEventListener("mousemove", (event) => { const track = root.querySelector(selector); if (!track) return; const box = track.getBoundingClientRect(); if (!box.width || event.clientX < box.left || event.clientX > box.right) { hide(); return; } positionCursor(Math.max(0, Math.min(100, (event.clientX - box.left) / box.width * 100))); }); root.addEventListener("mouseleave", hide); if (hoverRoot !== root) { hoverRoot.addEventListener("mouseover", (event) => { const target = event.target; const item = target && typeof target.closest === "function" ? target.closest("[data-event]") : undefined; if (item) positionCursor(Number(item.dataset.t), true); else hide(); }); hoverRoot.addEventListener("mouseleave", hide); } }
733
+ 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
+ function closeScript() { $("script-bg").classList.add("hidden"); $("script-modal").classList.add("hidden"); }
735
+ 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(); }
737
+ function connect() {
738
+ const generation = ++socketGeneration;
739
+ const Socket = window.WebSocket;
740
+ const socket = new Socket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`);
741
+ state.socket = socket;
742
+ socket.addEventListener("open", () => { if (state.socket === socket && generation === socketGeneration) socket.send(JSON.stringify({ type: "ui:attach" })); });
743
+ socket.addEventListener("message", (event) => {
744
+ if (state.socket !== socket || generation !== socketGeneration) return;
745
+ try {
746
+ const message = JSON.parse(event.data);
747
+ if (message.type === "state") acceptState(message);
748
+ 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 (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
+ 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
+ } catch { showToast("invalid Trajectory update"); }
751
+ });
752
+ socket.addEventListener("close", () => {
753
+ if (state.socket !== socket || generation !== socketGeneration) return;
754
+ state.socket = null;
755
+ state.transcriptPending = {};
756
+ state.actionTargets.clear();
757
+ socketReconnect = setTimeout(() => { socketReconnect = undefined; if (generation === socketGeneration) connect(); }, 1000);
758
+ });
759
+ }
760
+ $("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.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-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.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; renderInspector(entries[state.selectedEvent || 0] ? entryDetails(entries[state.selectedEvent || 0], agent, entries) : undefined, agent, entries); } else { state.sysPane = target.dataset.pane; 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; setView("agent"); } });
762
+ $("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(); });
764
+ $("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
+ 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
+ $("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
+ 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
+ window.addEventListener("resize", clampSwimHeight);
769
+ document.documentElement.dataset.theme = "tty"; 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
+ </script>
771
+ </body>
772
+ </html>