agents-relay 2.0.133 → 2.0.135

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.
@@ -1,188 +1,30 @@
1
+ import { dashboardStyles } from './dashboard-style.js';
2
+ import { workflowPresentation } from './dashboard-workflow-presentation.js';
1
3
  import { createRequire } from 'node:module';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
2
6
  export const dashboardVersion = createRequire(import.meta.url)('../package.json').version;
7
+ /** Resolve a full immutable source revision at server load. Without checkout metadata, omit links. */
8
+ export function dashboardSourceRevision() {
9
+ const override = process.env.AGENTS_RELAY_WORKFLOW_SOURCE_REF;
10
+ if (override && /^[0-9a-f]{40}$/i.test(override))
11
+ return override;
12
+ try {
13
+ const repoRoot = fileURLToPath(new URL('../', import.meta.url));
14
+ const revision = execFileSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
15
+ return /^[0-9a-f]{40}$/i.test(revision) ? revision : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
3
21
  export const dashboardHtml = `<!doctype html>
4
22
  <html lang="en" data-dashboard-contract="safe-dom,named-relay-events,snapshot-events,bounded-event-history">
5
23
  <head>
6
24
  <meta charset="utf-8">
7
25
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
26
  <title>Agents Relay Console</title>
9
- <style>
10
- :root { color-scheme: light dark; --bg:#f3f5f8; --surface:#fff; --surface2:#f8fafc; --border:#d8dee7; --text:#17202b; --muted:#687587; --accent:#285f86; --accent2:#e8f1f8; --ok:#19703a; --warn:#8b5b08; --bad:#a33c3c; --shadow:0 1px 2px rgba(15,23,42,.06); }
11
- * { box-sizing:border-box; }
12
- [hidden] { display:none !important; }
13
- body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,sans-serif; }
14
- header { position:sticky; top:0; z-index:3; background:var(--surface); border-bottom:1px solid var(--border); padding:.8rem 1rem; }
15
- .brand { display:flex; align-items:baseline; gap:.65rem; min-width:0; }
16
- .version { margin-left:auto; color:var(--muted); font-size:.72rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; white-space:nowrap; }
17
- .runtime-summary { display:flex; gap:.4rem; align-items:center; flex-wrap:wrap; margin-top:.55rem; }
18
- .runtime-stat { padding:.18rem .45rem; border:1px solid var(--border); border-radius:999px; background:var(--surface2); color:var(--muted); font-size:.72rem; white-space:nowrap; }
19
- .runtime-stat strong { color:var(--text); }
20
- h1 { margin:0; font-size:1.08rem; letter-spacing:-.02em; }
21
- .tagline,.subtle { color:var(--muted); font-size:.78rem; }
22
- .warning-banner { margin:.65rem 0 0; padding:.5rem .65rem; border:1px solid #e6bf62; border-radius:.4rem; background:#fff4d6; color:#6f4b00; font-size:.78rem; font-weight:700; }
23
- .tagline { margin:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
24
- nav[role="tablist"] { display:flex; gap:.3rem; margin-top:.65rem; }
25
- nav button { border:0; border-radius:.35rem; background:transparent; color:var(--muted); padding:.38rem .68rem; font:inherit; font-weight:700; cursor:pointer; }
26
- nav button[aria-selected="true"] { background:var(--accent); color:#fff; }
27
- button:focus-visible,a:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
28
- main { max-width:1500px; margin:auto; padding:1rem; display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:1rem; align-items:start; }
29
- .panel { min-width:0; background:var(--surface); border:1px solid var(--border); border-radius:.55rem; box-shadow:var(--shadow); }
30
- .panel-head { display:flex; align-items:center; justify-content:space-between; gap:.6rem; padding:.65rem .75rem; border-bottom:1px solid var(--border); }
31
- .panel-head h2,.panel-head h3 { margin:0; color:var(--muted); font-size:.75rem; letter-spacing:.07em; text-transform:uppercase; min-width:0; }\n #workspace-heading { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n #selected-job-title { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
32
- .panel-actions { display:flex; align-items:center; gap:.45rem; }
33
- .job-filters { display:flex; gap:.35rem; align-items:center; flex-wrap:wrap; }
34
- .job-filter { max-width:145px; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--text); padding:.27rem .4rem; font:inherit; font-size:.72rem; }
35
- .job-search { width:95px; max-width:14vw; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--text); padding:.27rem .45rem; font:inherit; font-size:.72rem; }
36
- .quick-filter { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.27rem .42rem; font:inherit; font-size:.7rem; font-weight:750; cursor:pointer; }
37
- .quick-filter[aria-pressed="true"] { border-color:var(--accent); background:var(--accent2); color:var(--accent); }
38
- .refresh-button { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.28rem .55rem; font:inherit; font-size:.72rem; font-weight:700; cursor:pointer; }
39
- .refresh-button:disabled { cursor:default; opacity:.55; }
40
- .mobile-only { display:none; }
41
- .panel-body { padding:.75rem; overflow:auto; max-height:calc(100vh - 11.5rem); }
42
- #jobs-workspace > .panel:first-child .panel-body { overflow:visible; max-height:none; }
43
- .jobs-panel { grid-row:1/span 2; }
44
- .workspace { display:grid; gap:1rem; }
45
- .job-list,.tree,.event-list { list-style:none; margin:0; padding:0; }
46
- .job-list { display:grid; gap:.35rem; }
47
- .job-row { display:grid; gap:.2rem; padding:.35rem .45rem; border:1px solid transparent; border-radius:.4rem; cursor:pointer; }
48
- .job-row[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
49
- .job-row.complete-unmerged { border-color:#d69a1b; background:#fff8df; }
50
- .job-row.complete-unmerged[aria-current="true"] { border-color:#b77900; background:#fff0bd; }
51
- .job-content { min-width:0; display:grid; gap:.2rem; }
52
- .job-button { min-width:0; display:block; padding:.12rem 0; border:0; background:transparent; color:inherit; text-align:left; font:inherit; cursor:pointer; }
53
- .job-title { font-weight:750; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
54
- .job-last-line { display:flex; align-items:center; justify-content:space-between; gap:.5rem; min-width:0; }
55
- .job-last-line .subtle:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
56
- .job-last-line .subtle:last-child { flex:none; white-space:nowrap; }
57
- .job-primary { display:flex; align-items:center; justify-content:space-between; gap:.45rem; min-width:0; }
58
- .job-status-line { display:flex; align-items:center; gap:.3rem; min-width:0; white-space:nowrap; }
59
- .job-status-line .badge { flex:0 0 auto; }
60
- .status-pill,.badge { display:inline-flex; align-items:center; border-radius:999px; padding:.06rem .42rem; min-height:1.35rem; font-size:.68rem; font-weight:800; }
61
- .status-pill.ok,.badge.ACTIVE,.badge.RUNNING,.badge.SUCCEEDED,.badge.COMPLETED,.badge.MERGED { background:#e5f3e9; color:var(--ok); }
62
- .status-pill.warn,.badge.BLOCKED,.badge.DRAFT,.badge.QUEUED,.badge.WAITING,.badge.OPEN { background:#fff1d2; color:var(--warn); }
63
- .status-pill.bad,.badge.FAILED { background:#ffe8e8; color:var(--bad); }
64
- .status-pill.neutral,.badge.IDLE,.badge.CANCELLED { background:#edf0f4; color:var(--muted); }
65
- .badge.DRAFT { background:#eef2ff; color:#3f51b5; }
66
- .badge.MERGE_PENDING { background:#ffe3a3; color:#7a4a00; }
67
- .details-dialog { width:min(850px,calc(100vw - 2rem)); max-height:85vh; overflow:auto; border:1px solid var(--border); border-radius:.6rem; background:var(--surface); color:var(--text); box-shadow:0 18px 55px #0004; padding:1rem; } .details-dialog::backdrop { background:#17202b99; } .details-dialog-head { display:flex; justify-content:space-between; align-items:center; gap:1rem; position:sticky; top:-1rem; background:var(--surface); padding:.5rem 0; } .details-dialog pre { white-space:pre-wrap; overflow-wrap:anywhere; background:var(--surface2); padding:.7rem; border-radius:.4rem; } .details-dialog h3 { font-size:.8rem; margin:1rem 0 .3rem; }
68
- .job-summary,.detail-groups { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.55rem; }
69
- .detail-group { min-width:0; padding:.5rem; background:var(--surface2); border:1px solid var(--border); border-radius:.4rem; }
70
- .detail-label { margin:0; color:var(--muted); font-size:.66rem; text-transform:uppercase; letter-spacing:.04em; }
71
- .detail-value { margin:.12rem 0 0; font-weight:650; overflow-wrap:anywhere; }
72
- .section-title { margin:.8rem 0 .35rem; color:var(--muted); font-size:.72rem; letter-spacing:.05em; text-transform:uppercase; }
73
- ul.tree { display:grid; gap:.25rem; min-width:0; max-width:100%; }
74
- ul.tree li { min-width:0; max-width:100%; }
75
- ul.tree ul { width:calc(100% - .6rem); min-width:0; max-width:calc(100% - .6rem); list-style:none; margin:.2rem 0 .1rem .6rem; padding-left:.6rem; border-left:1px solid var(--border); }
76
- .task-line { display:flex; align-items:center; gap:.2rem; min-width:0; }
77
- .task-fold { flex:0 0 auto; width:1.35rem; padding:.32rem 0; border:0; background:transparent; color:var(--muted); font:inherit; cursor:pointer; }
78
- .task-row { width:100%; max-width:100%; overflow:hidden; flex:1 1 0; display:flex; align-items:center; gap:.4rem; min-width:0; padding:.32rem .38rem; border:0; border-radius:.35rem; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
79
- .task-row[aria-current="true"] { background:var(--accent2); }
80
- .task-row .badge { flex:0 0 auto; }
81
- .task-id { min-width:0; max-width:100%; flex:1 1 0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font: .77rem ui-monospace,SFMono-Regular,Menlo,monospace; }
82
- .task-actions { display:flex; justify-content:flex-end; gap:.25rem; margin-bottom:.5rem; }
83
- .task-link { color:var(--accent); font-weight:700; }
84
- .agent-graph { display:grid; gap:.45rem; margin-top:.55rem; padding:.55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); overflow:auto; }
85
- .agent-wave { display:flex; align-items:center; gap:.4rem; min-width:max-content; }
86
- .agent-node { padding:.32rem .48rem; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); font:.73rem ui-monospace,SFMono-Regular,Menlo,monospace; }
87
- .agent-arrow { color:var(--muted); font-weight:800; }
88
- .agent-graph-source { margin:.1rem 0 0; padding:.45rem; border-radius:.35rem; background:var(--surface); color:var(--muted); font:.7rem/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; overflow:auto; }
89
- .event-list { display:grid; gap:.45rem; }
90
- .event-item { padding:.34rem .42rem .42rem; border-left:3px solid transparent; border-bottom:1px solid var(--border); border-radius:.28rem; }
91
- .event-item.event-error { border-left-color:var(--bad); background:#fff4f4; }
92
- .event-item.event-error .event-kind,.event-item.event-error .event-message { color:var(--bad); }
93
- .event-item.event-warn { border-left-color:var(--warn); background:#fff9eb; }
94
- .event-item.event-warn .event-kind { color:var(--warn); }
95
- .event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
96
- .event-kind { color:var(--accent); font-weight:800; }
97
- .event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
98
- #usage-view,#graphs-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
99
- .graphs-browser { display:grid; grid-template-columns:minmax(190px,260px) minmax(0,1fr); gap:.75rem; min-height:60vh; }
100
- .graph-list { display:grid; align-content:start; gap:.35rem; }
101
- .graph-button { width:100%; display:grid; gap:.08rem; padding:.48rem .55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--text); text-align:left; font:inherit; cursor:pointer; }
102
- .graph-button[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
103
- .graph-button strong { font-size:.78rem; }
104
- .graph-button span { color:var(--muted); font:.66rem ui-monospace,SFMono-Regular,Menlo,monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
105
- .graph-preview { min-width:0; display:grid; align-content:start; gap:.55rem; }
106
- .graph-preview-head { display:flex; justify-content:space-between; gap:.7rem; align-items:baseline; flex-wrap:wrap; }
107
- .graph-preview-head h3 { margin:0; font-size:.95rem; }
108
- .graph-canvas { min-height:24rem; display:grid; place-items:center; overflow:auto; padding:1rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
109
- .graph-canvas svg { max-width:none; height:auto; }
110
- .graph-source { margin:0; max-height:22rem; overflow:auto; padding:.7rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--muted); font:.72rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; }
111
- .graph-source-details summary { cursor:pointer; color:var(--muted); font-size:.74rem; font-weight:700; }
112
- .rules-editor { display:grid; gap:.65rem; }
113
- .rules-help { margin:0; color:var(--muted); font-size:.8rem; }
114
- .rules-textarea { width:100%; min-height:55vh; resize:vertical; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); color:var(--text); padding:.8rem; font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; }
115
- .rules-footer { display:flex; align-items:center; justify-content:space-between; gap:.75rem; flex-wrap:wrap; }
116
- .rules-save { border:1px solid var(--accent); border-radius:.35rem; background:var(--accent); color:#fff; padding:.38rem .8rem; font:inherit; font-weight:750; cursor:pointer; }
117
- .rules-save:disabled { opacity:.55; cursor:default; }
118
- .usage-providers { display:grid; gap:.55rem; }
119
- .usage-provider { display:grid; grid-template-columns:minmax(130px,180px) minmax(0,1fr); gap:1rem; align-items:center; padding:.7rem .8rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
120
- .usage-provider h3 { margin:0; font-size:.86rem; }
121
- .usage-bars { display:grid; gap:1rem; }
122
- .usage-row { display:grid; gap:.35rem; }
123
- .usage-label { display:flex; justify-content:space-between; gap:1rem; font-weight:700; }
124
- .usage-track { height:.72rem; overflow:hidden; border:1px solid var(--border); border-radius:999px; background:var(--surface2); }
125
- .usage-fill { height:100%; width:0; border-radius:inherit; background:var(--accent); transition:width .2s ease; }
126
- .usage-details { border-top:1px solid var(--border); padding-top:.45rem; }
127
- .usage-details summary { cursor:pointer; color:var(--muted); font-size:.72rem; font-weight:700; }
128
- .usage-details-body { display:grid; gap:.3rem; margin-top:.4rem; overflow-wrap:anywhere; }
129
- .empty,.error-state { padding:.85rem; border:1px dashed var(--border); border-radius:.4rem; color:var(--muted); }
130
- .error-state,.error-text { color:var(--bad); }
131
- .skeleton { height:3.2rem; border-radius:.4rem; background:linear-gradient(90deg,#edf0f4,#fafbfc,#edf0f4); animation:pulse 1.2s infinite; }
132
- @keyframes pulse { 50% { opacity:.55; } }
133
- @media (max-width:1050px) { main { grid-template-columns:1fr; } .jobs-panel { grid-row:auto; } .panel-body { max-height:24rem; } #jobs-workspace > .panel:first-child .panel-body { max-height:none; } }
134
- @media (max-width:650px) {
135
- header { padding:.65rem .7rem; }
136
- nav[role="tablist"] { overflow-x:auto; padding-bottom:.1rem; }
137
- nav button { flex:0 0 auto; white-space:nowrap; }
138
- main { padding:.5rem; gap:.65rem; }
139
- body[data-mobile-jobs-view="list"] #jobs-workspace { display:none; }
140
- body[data-mobile-jobs-view="detail"] #jobs-view { display:none; }
141
- .mobile-only { display:inline-flex; }
142
- #jobs-view .panel-head { display:grid; align-items:start; }
143
- #jobs-view .panel-actions { width:100%; flex-wrap:wrap; }
144
- .runtime-summary { display:none; }
145
- #jobs-view .job-filters { width:auto; }
146
- #jobs-view .job-search,
147
- #jobs-view #quick-status,
148
- #jobs-view #quick-autonomous,
149
- #jobs-view #quick-draft { display:none; }
150
- #jobs-view .panel-body { max-height:none; }
151
- #jobs-workspace { gap:.65rem; }
152
- #jobs-workspace .panel-body { max-height:none; }
153
- #usage-view .panel-head { padding:.6rem .7rem .35rem; }
154
- #usage-view .panel-head .subtle { display:none; }
155
- .graphs-browser { grid-template-columns:1fr; min-height:0; }
156
- .graph-list { grid-template-columns:repeat(2,minmax(0,1fr)); }
157
- .graph-canvas { min-height:18rem; }
158
- #usage-view .panel-body { padding:.45rem .5rem .65rem; max-height:none; }
159
- #usage-view .usage-providers { gap:.45rem; }
160
- #usage-view .usage-provider {
161
- grid-template-columns:1fr;
162
- gap:.55rem;
163
- padding:.65rem .7rem;
164
- border-radius:.55rem;
165
- background:var(--surface);
166
- }
167
- #usage-view .usage-provider h3 { font-size:.78rem; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; }
168
- #usage-view .usage-bars { gap:.7rem; }
169
- #usage-view .usage-row { gap:.25rem; }
170
- #usage-view .usage-label { align-items:baseline; gap:.5rem; font-size:.78rem; }
171
- #usage-view .usage-label span:last-child { font-size:1.05rem; font-weight:800; color:var(--text); }
172
- #usage-view .usage-track { height:.48rem; border:0; background:var(--border); }
173
- #usage-view .usage-row .subtle { font-size:.68rem; line-height:1.25; }
174
- #usage-view .usage-provider > .detail-value { font-size:1.2rem; font-weight:800; }
175
- #usage-view .usage-details { padding-top:.35rem; }
176
- #usage-view .usage-details-body { font-size:.68rem; }
177
- .details-dialog { width:min(850px,calc(100vw - 2rem)); max-height:85vh; overflow:auto; border:1px solid var(--border); border-radius:.6rem; background:var(--surface); color:var(--text); box-shadow:0 18px 55px #0004; padding:1rem; } .details-dialog::backdrop { background:#17202b99; } .details-dialog-head { display:flex; justify-content:space-between; align-items:center; gap:1rem; position:sticky; top:-1rem; background:var(--surface); padding:.5rem 0; } .details-dialog pre { white-space:pre-wrap; overflow-wrap:anywhere; background:var(--surface2); padding:.7rem; border-radius:.4rem; } .details-dialog h3 { font-size:.8rem; margin:1rem 0 .3rem; }
178
- .job-summary,.detail-groups { grid-template-columns:1fr; }
179
- .panel-head { padding:.55rem .6rem; }
180
- .panel-body { padding:.6rem; }
181
- .tagline { display:none; }
182
- }
183
- @media (prefers-reduced-motion:reduce) { .skeleton { animation:none; } .usage-fill { transition:none; } }
184
- .router-rules-workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start;min-width:0}.router-rules-pane{min-width:0;max-width:100%;overflow:hidden}.rules-editor .rules-textarea{min-height:420px}.audit-log{display:flex;flex-direction:column;gap:8px;max-height:500px;overflow:auto;min-width:0}.audit-entry{border:1px solid var(--border);border-radius:10px;padding:10px;background:var(--surface)}.audit-entry-head{display:flex;justify-content:space-between;gap:8px;align-items:center}.audit-entry pre{white-space:pre-wrap;word-break:break-word;margin:8px 0 0;font-size:11px}.router-test-panel{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}.router-test-results{margin-top:8px;display:flex;flex-direction:column;gap:6px;max-height:480px;overflow:auto;min-width:0}.router-test-results pre{white-space:pre;overflow:auto;max-width:100%;font-size:11px;line-height:1.45;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px}.router-test-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:7px 0;border-bottom:1px solid var(--border)}@media(max-width:900px){.router-rules-workspace{grid-template-columns:1fr}.router-rules-pane{max-height:none}.router-test-results{max-height:420px}}@media(max-width:650px){#rules-view .panel-head .subtle{display:none}#rules-view .panel-body{max-height:none;overflow:visible;padding:.6rem}#rules-view .router-rules-workspace{gap:.9rem}#rules-view .rules-help{font-size:.74rem;line-height:1.4}#rules-view .rules-textarea{min-height:52vh;padding:.65rem;font-size:12px}#rules-view .rules-footer{align-items:stretch}#rules-view .rules-footer>.subtle{width:100%}#rules-view .rules-footer>div{width:100%;display:grid;grid-template-columns:1fr 1fr;gap:.5rem}#rules-view .rules-save{min-height:44px;width:100%;padding:.55rem .7rem}#rules-view .router-rules-pane>.panel-head{padding:.55rem 0}#rules-view .audit-log{max-height:none}#rules-view .audit-entry-head{align-items:flex-start}#rules-view .audit-entry-head .subtle{display:block;flex:0 0 auto}#rules-view .router-test-panel{min-width:0;max-width:100%;overflow:hidden}#rules-view .router-test-results{max-height:none;width:100%;max-width:100%;overflow-x:hidden}#rules-view .router-test-results>*{min-width:0;max-width:100%}#rules-view .router-test-results pre{width:100%;box-sizing:border-box;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:11px;padding:.65rem}#rules-view .router-test-row{grid-template-columns:1fr}}
185
- </style>
27
+ ${dashboardStyles}
186
28
  </head>
187
29
  <body>
188
30
  <header>
@@ -194,7 +36,7 @@ export const dashboardHtml = `<!doctype html>
194
36
  <button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
195
37
  <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Router Rules</button>
196
38
  <button id="shortcuts-tab" type="button" role="tab" aria-selected="false" aria-controls="shortcuts-view">Shortcuts</button>
197
- <button id="graphs-tab" type="button" role="tab" aria-selected="false" aria-controls="graphs-view">Graphs</button>
39
+ <button id="graphs-tab" type="button" role="tab" aria-selected="false" aria-controls="graphs-view">Workflow</button>
198
40
  </nav>
199
41
  </header>
200
42
  <main>
@@ -231,7 +73,10 @@ export const dashboardHtml = `<!doctype html>
231
73
  <div class="graphs-browser">
232
74
  <div id="graph-list" class="graph-list" aria-label="Workflow graphs"></div>
233
75
  <div class="graph-preview">
234
- <div class="graph-preview-head"><h3 id="graph-title">Select a graph</h3><span id="graph-path" class="subtle"></span></div>
76
+ <div id="graph-navigation" class="graph-navigation" aria-label="Graph navigation"><button type="button" id="graph-back" class="refresh-button">Back</button><button type="button" id="graph-forward" class="refresh-button">Forward</button><button type="button" id="graph-entry" class="refresh-button">Entry</button><span id="graph-breadcrumb" class="subtle"></span></div>
77
+ <div id="graph-detail" class="graph-source" aria-live="polite" hidden></div>
78
+ <div id="graph-links" class="graph-navigation" aria-label="Linked subgraphs"></div>
79
+ <div class="graph-preview-head"><h3 id="graph-title">Select a graph</h3><button type="button" id="graph-maximize" class="refresh-button">Maximize graph</button><span id="graph-path" class="subtle"></span></div>
235
80
  <div id="graph-canvas" class="graph-canvas"><p class="empty">Select a graph to inspect it.</p></div>
236
81
  <details class="graph-source-details"><summary>Mermaid source</summary><pre id="graph-source" class="graph-source"></pre></details>
237
82
  </div>
@@ -239,6 +84,7 @@ export const dashboardHtml = `<!doctype html>
239
84
  </div>
240
85
  </div>
241
86
  </section>
87
+ <dialog id="graph-dialog" class="graph-dialog" aria-label="Workflow graph viewer"><div class="graph-dialog-header"><strong id="graph-dialog-title">Workflow graph</strong><div class="graph-navigation"><button type="button" id="graph-zoom-out" class="refresh-button" aria-label="Zoom out">−</button><span id="graph-zoom-level" aria-live="polite">100%</span><button type="button" id="graph-zoom-in" class="refresh-button" aria-label="Zoom in">+</button><button type="button" id="graph-zoom-reset" class="refresh-button">Fit</button><button type="button" id="graph-dialog-close" class="refresh-button">Close</button></div></div><div id="graph-dialog-viewport" class="graph-dialog-viewport"></div></dialog>
242
88
  <section id="rules-view" role="tabpanel" aria-labelledby="rules-tab" hidden><div class="panel"><div class="panel-head"><h2>Router Rules</h2><span id="rules-meta" class="subtle"></span></div><div class="panel-body"><div class="router-rules-workspace"><div class="router-rules-pane rules-editor"><h3>Rules Editor</h3><p class="rules-help">Execution Router rules with an explicit <code>[choices]</code> section (provider: model [reasoning] entries, additive to profile defaults) and a <code>[routing-instruction]</code> section forwarded verbatim to Laya. Relay deduplicates, applies hard gates, and sends one merged choice list; impossible routes remain excluded by code and Codex stays the deterministic fallback.</p><textarea id="rules-text" class="rules-textarea" spellcheck="true" aria-label="Execution router rules" placeholder="[choices]\nopenai: sol medium, luna high\nzai: glm-5.3-flash high\n\n[routing-instruction]\nPrefer zai until its quota resets; otherwise use the openai default."></textarea><div class="rules-footer"><span id="rules-status" class="subtle" aria-live="polite"></span><div><button id="rules-test" class="rules-save" type="button">Test Router</button> <button id="rules-save" class="rules-save" type="button">Save rules</button></div></div></div><div class="router-rules-pane"><div class="panel-head"><h3>Audit Log</h3><button id="audit-refresh" type="button">Refresh</button></div><div id="audit-log" class="audit-log" aria-live="polite"><p class="empty">Loading latest routing decisions…</p></div></div></div><div class="router-test-panel"><h3>Test Router Result</h3><div id="router-test-results" class="router-test-results" aria-live="polite"><p class="empty">Select a Job and Task, then run Test Router.</p></div></div></div></div></section>
243
89
  <section id="shortcuts-view" role="tabpanel" aria-labelledby="shortcuts-tab" hidden>
244
90
  <div class="panel">
@@ -323,7 +169,10 @@ let quickMode=sessionStorage.getItem('agents-relay:quick-filter')||'jobs';
323
169
  let selectedRuntimeStatus=sessionStorage.getItem('agents-relay:runtime-status-filter')||'';
324
170
  let searchQuery=sessionStorage.getItem('agents-relay:job-search')||'';
325
171
  let workflowGraphs=[];
326
- let selectedGraphId=sessionStorage.getItem('agents-relay:selected-graph')||'';
172
+ let selectedGraphId='entry';
173
+ const GRAPH_ENTRY_SOURCE=${JSON.stringify(workflowPresentation.entrySource)};
174
+ const GRAPH_NAV=${JSON.stringify(workflowPresentation.navigation)};
175
+ const WORKFLOW_SOURCE_REF=${JSON.stringify(dashboardSourceRevision())};
327
176
  let eventTaskId=selectedTaskId;
328
177
  const collapsedTasks=new Set();
329
178
  const expandedTasks=new Set();
@@ -447,11 +296,26 @@ function githubUsageCard(effective,status){
447
296
  return card;
448
297
  }
449
298
  async function drawUsage(){try{const overview=await loadJSON('/api/usage');usageRoot.replaceChildren();usageMeta.textContent='Real account telemetry when available';const providers=Array.isArray(overview.providers)?overview.providers:[];const credits=Array.isArray(overview.credits)?overview.credits:[];const grid=element('div','usage-providers');if(overview.githubEffective||overview.githubStatus)grid.append(githubUsageCard(overview.githubEffective,overview.githubStatus));for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));if(!grid.children.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
450
- function selectedWorkflowGraph(){return workflowGraphs.find(graph=>graph.id===selectedGraphId)||workflowGraphs[0]||null;}
451
- function renderGraphList(){graphList.replaceChildren();if(!workflowGraphs.length){graphList.append(element('p','empty',text('No workflow graphs found.')));return;}for(const graph of workflowGraphs){const button=document.createElement('button');button.type='button';button.className='graph-button';button.setAttribute('aria-current',String(graph.id===selectedGraphId));button.append(element('strong','',text(graph.name)),element('span','',text(graph.path)));button.addEventListener('click',()=>{selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);renderGraphList();void renderSelectedGraph();});graphList.append(button);}}
452
- async function renderSelectedGraph(){const graph=selectedWorkflowGraph();if(!graph){graphTitle.textContent='Select a graph';graphPath.textContent='';graphSource.textContent='';graphCanvas.replaceChildren(element('p','empty',text('No workflow graphs found.')));return;}selectedGraphId=graph.id;sessionStorage.setItem('agents-relay:selected-graph',selectedGraphId);graphTitle.textContent=graph.name;graphPath.textContent=graph.path;graphSource.textContent=graph.source;graphCanvas.replaceChildren(element('p','subtle',text('Rendering…')));try{if(!window.mermaid)throw new Error('Mermaid renderer unavailable');if(!window.__relayMermaidReady){window.mermaid.initialize({startOnLoad:false,securityLevel:'strict'});window.__relayMermaidReady=true;}const rendered=await window.mermaid.render('relay-graph-'+graph.id.replace(/[^A-Za-z0-9_-]/g,'-'),graph.source);const parsed=new DOMParser().parseFromString(rendered.svg,'image/svg+xml');const svg=parsed.documentElement;if(svg.nodeName.toLowerCase()!=='svg')throw new Error('Mermaid did not return SVG');graphCanvas.replaceChildren(document.importNode(svg,true));}catch(error){graphCanvas.replaceChildren(element('p','error-state',text('Visual preview unavailable: '+error.message)));}}
453
- async function drawGraphs(){try{const graphs=await loadJSON('/api/graphs');workflowGraphs=Array.isArray(graphs)?graphs:[];if(!workflowGraphs.some(graph=>graph.id===selectedGraphId))selectedGraphId=workflowGraphs[0]?.id||'';graphsMeta.textContent=workflowGraphs.length+' graph'+(workflowGraphs.length===1?'':'s')+' · workflow/*.mmd';renderGraphList();await renderSelectedGraph();}catch(error){graphsMeta.textContent='Unavailable';workflowGraphs=[];renderGraphList();graphCanvas.replaceChildren(element('p','error-state',text('Graphs unavailable: '+error.message)));}}
454
- function routeWorker(record){const id=record?.selected?.worker?.id||record?.worker?.id||'';return String(id).startsWith('chatgpt')?'ChatGPT':String(id).startsWith('codex')?'Codex':value(id);}
299
+ function selectedWorkflowGraph(){return workflowGraphs.find(graph=>graph.id===selectedGraphId)||null;}
300
+ function graphTargets(id){return GRAPH_NAV[id]||[];}
301
+ function graphFromHash(){if(location.hash==='#/graphs')return 'entry';if(!location.hash.startsWith('#/graphs/'))return null;const id=location.hash.slice(9);return /^[a-z0-9-]+$/.test(id)?id:null;}
302
+ const WORKFLOW_NAV_KEY='agents-relay:workflow-navigation';
303
+ function graphHistory(){try{const value=JSON.parse(sessionStorage.getItem(WORKFLOW_NAV_KEY)||'null');if(value&&Array.isArray(value.entries)&&value.entries.every(id=>typeof id==='string'&&/^[a-z0-9-]+$/.test(id)))return value;}catch{}return {entries:[]};}
304
+ function saveGraphHistory(data){sessionStorage.setItem(WORKFLOW_NAV_KEY,JSON.stringify(data));}
305
+ function graphHistoryIndex(){const n=history.state?.workflowIndex;return Number.isInteger(n)&&n>=0?n:null;}
306
+ function initializeGraphHistory(){const route=graphFromHash();if(route===null)return;let data=graphHistory();const index=graphHistoryIndex();if(index!==null&&data.entries[index]===route)return;data={entries:[route]};saveGraphHistory(data);history.replaceState({...history.state,workflowIndex:0,workflowPath:route==='entry'?['entry']:['entry',route]},'');}
307
+ function navigateGraph(id){if(id!=='entry'&&!workflowGraphs.some(graph=>graph.id===id))return;if(graphFromHash()===null)return;initializeGraphHistory();const data=graphHistory();const current=graphHistoryIndex()??0;const path=history.state?.workflowPath;const valid=Array.isArray(path)&&path.at(-1)===graphFromHash()&&path[0]==='entry';const nextPath=id==='entry'?['entry']:[...(valid?path:['entry']),id];data.entries=data.entries.slice(0,current+1);data.entries.push(id);saveGraphHistory(data);history.pushState({workflowIndex:current+1,workflowPath:nextPath},'','#/graphs/'+id);syncGraphRoute();}
308
+ function syncGraphRoute(){const route=graphFromHash();if(route===null)return;initializeGraphHistory();selectedGraphId=route;renderGraphList();void renderSelectedGraph();}
309
+ function renderGraphNavigation(){const graph=selectedWorkflowGraph();initializeGraphHistory();const data=graphHistory();const index=graphHistoryIndex()??0;document.querySelector('#graph-back').disabled=index<=0;document.querySelector('#graph-forward').disabled=index>=data.entries.length-1;const path=history.state?.workflowPath;const names=Array.isArray(path)&&path.at(-1)===selectedGraphId?path:[selectedGraphId==='entry'?'entry':'entry',...(selectedGraphId==='entry'?[]:[selectedGraphId])];document.querySelector('#graph-breadcrumb').textContent=names.map(id=>id==='entry'?'Entry (conceptual)':workflowGraphs.find(g=>g.id===id)?.name||id).join(' / ');const links=document.querySelector('#graph-links');links.replaceChildren();for(const id of graphTargets(selectedGraphId)){if(!workflowGraphs.some(g=>g.id===id))continue;const button=element('button','refresh-button',text('Open '+id));button.type='button';button.addEventListener('click',()=>navigateGraph(id));links.append(button);}}
310
+ function renderGraphList(){graphList.replaceChildren();if(!workflowGraphs.length){graphList.append(element('p','empty',text('No workflow graphs found.')));return;}for(const graph of workflowGraphs){const button=document.createElement('button');button.type='button';button.className='graph-button';button.setAttribute('aria-current',String(graph.id===selectedGraphId));button.append(element('strong','',text(graph.name)),element('span','',text(graph.path)));button.addEventListener('click',()=>navigateGraph(graph.id));graphList.append(button);}}
311
+ async function graphDetail(kind,name){const panel=document.querySelector('#graph-detail');panel.hidden=false;panel.replaceChildren(element('strong','',text(kind+': '+name)),element('p','subtle',text('Loading authoritative references…')));try{const params=new URLSearchParams({graph:selectedGraphId,kind,name});const result=await loadJSON('/api/graph-symbol?'+params);panel.replaceChildren(element('strong','',text(kind+': '+name)));const sections=[['Graph',[result.graph]],['Canonical contract',result.contracts],['Implementation',result.implementation],['Tests',result.tests]];for(const [heading,refs] of sections){panel.append(element('p','detail-label',text(heading)));if(!refs?.length){panel.append(element('p','subtle',text('No exact symbol reference found.')));continue;}for(const ref of refs){const link=element('a','',text(ref.path+(ref.line>0?':'+ref.line:'')+' ['+(ref.evidence==='exact-token'?'verified graph token':'text-search suggestion')+']'));if(WORKFLOW_SOURCE_REF){link.href='https://github.com/lalalic/agents-relay/blob/'+WORKFLOW_SOURCE_REF+'/'+ref.path+(ref.line>0?'#L'+ref.line:'');link.target='_blank';link.rel='noopener noreferrer';}else{link.removeAttribute('href');link.title='Source revision unavailable';}link.target='_blank';link.rel='noopener noreferrer';panel.append(link,document.createTextNode(' '));}}panel.scrollIntoView({block:'nearest'});}catch(error){panel.append(element('p','error-state',text('Reference lookup unavailable: '+error.message)));}}
312
+ function interactiveNode(node,callback,label){node.classList.add('drillable');node.setAttribute('tabindex','0');node.setAttribute('role','button');node.setAttribute('aria-label',label);node.addEventListener('click',callback);node.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();callback();}});}
313
+ function graphNodeDefinitions(source){const result=new Map();for(const line of source.split('\\n')){const pattern=/([A-Za-z0-9_]+)\\["([^"\\n]+)"\\]/g;for(const match of line.matchAll(pattern))result.set(match[1],match[2]);}return result;}
314
+ function addNodeLabel(node,label){const ns='http://www.w3.org/2000/svg';const old=node.querySelector('g.label');if(old)old.remove();const textNode=document.createElementNS(ns,'text');textNode.setAttribute('x','0');textNode.setAttribute('y','0');textNode.setAttribute('text-anchor','middle');textNode.setAttribute('dominant-baseline','middle');textNode.setAttribute('fill','#182536');textNode.setAttribute('font-size','13');const rect=node.querySelector('rect.label-container,rect.basic,rect');const width=Math.max(65,Number(rect?.getAttribute('width')||135)-14);const maxChars=Math.max(9,Math.floor(width/7.2));const words=label.split(/\\s+/);const lines=[];let current='';for(const word of words){if(current&&(current+' '+word).length>maxChars){lines.push(current);current=word;}else current=current?current+' '+word:word;}if(current)lines.push(current);for(let i=0;i<lines.length;i++){const tspan=document.createElementNS(ns,'tspan');tspan.setAttribute('x','0');tspan.setAttribute('dy',i===0?String(-((lines.length-1)*7)):'14');tspan.textContent=lines[i];textNode.append(tspan);}node.append(textNode);}
315
+ function sanitizeGraphSvg(markup){const parsed=new DOMParser().parseFromString(markup,'image/svg+xml');const source=parsed.documentElement;if(source.localName!=='svg'||parsed.querySelector('parsererror'))throw new Error('Invalid SVG');const ns='http://www.w3.org/2000/svg';const elements=new Set(['svg','g','path','rect','circle','ellipse','line','polyline','polygon','text','tspan','defs','marker','title','desc','clipPath']);const attributes=new Set(['id','class','viewBox','width','height','x','y','x1','y1','x2','y2','cx','cy','r','rx','ry','d','points','transform','fill','stroke','stroke-width','stroke-dasharray','stroke-linecap','stroke-linejoin','opacity','fill-opacity','stroke-opacity','font-size','font-family','font-weight','text-anchor','dominant-baseline','markerWidth','markerHeight','markerUnits','refX','refY','orient','preserveAspectRatio','clip-path','marker-start','marker-mid','marker-end','role','aria-label']);const clean=node=>{if(node.nodeType===3)return document.createTextNode(node.textContent||'');if(node.nodeType!==1||node.namespaceURI!==ns||!elements.has(node.localName))return null;const out=document.createElementNS(ns,node.localName);for(const attr of node.attributes){if(!attr.namespaceURI&&attr.name==='style'){for(const declaration of attr.value.split(';')){const split=declaration.indexOf(':');if(split<0)continue;const property=declaration.slice(0,split).trim();const cssValue=declaration.slice(split+1).trim();if((property==='stroke'||property==='fill')&&/^(?:#[0-9a-fA-F]{3,8}|rgb\\([0-9.,%\\s]+\\)|none|currentColor)$/.test(cssValue))out.setAttribute(property,cssValue);else if(property==='stroke-width'&&/^[0-9]+(?:\\.[0-9]+)?(?:px)?$/.test(cssValue))out.setAttribute(property,cssValue);}continue;}if(attr.namespaceURI||!attributes.has(attr.name))continue;if(['marker-start','marker-mid','marker-end','clip-path'].includes(attr.name)){if(!attr.value.startsWith('url(#')||!attr.value.endsWith(')')||!(/^[a-zA-Z0-9_-]+$/.test(attr.value.slice(5,-1))))continue;}else if(attr.value.toLowerCase().includes('url(')||attr.value.includes('<')||attr.value.includes('>'))continue;out.setAttribute(attr.name,attr.value);}for(const child of node.childNodes){const safe=clean(child);if(safe)out.appendChild(safe);}return out;};return clean(source);}
316
+ function applyGraphPalette(svg){const ns='http://www.w3.org/2000/svg';for(const path of svg.querySelectorAll('path')){if(path.closest('marker')){if(!path.hasAttribute('fill'))path.setAttribute('fill','#65768a');continue;}if(!path.hasAttribute('fill'))path.setAttribute('fill','none');if(!path.hasAttribute('stroke'))path.setAttribute('stroke','#65768a');}for(const node of svg.querySelectorAll('g.node')){const error=node.classList.contains('failure');for(const shape of node.querySelectorAll('rect,polygon,circle,ellipse')){shape.setAttribute('fill',error?'#ffe4e4':'#f9fbff');shape.setAttribute('stroke',error?'#be3030':'#657f9e');shape.setAttribute('stroke-width','1.3');}for(const label of node.querySelectorAll('text'))label.setAttribute('fill',error?'#7c1818':'#182536');}for(const rect of svg.querySelectorAll('rect')){if(!rect.closest('g.node')&&!rect.hasAttribute('fill'))rect.setAttribute('fill','#ffffff');}for(const label of svg.querySelectorAll('text'))if(!label.hasAttribute('fill'))label.setAttribute('fill','#182536');}
317
+ async function renderSelectedGraph(){const graph=selectedWorkflowGraph();renderGraphNavigation();document.querySelector('#graph-detail').hidden=true;if(!graph){graphTitle.textContent='Graph not found';graphPath.textContent='';graphSource.textContent='';graphCanvas.replaceChildren(element('p','error-state',text('Graph not found: '+selectedGraphId+'. Select Entry or another graph.')));return;}graphTitle.textContent=graph.name;graphPath.textContent=graph.path;graphSource.textContent=graph.source;graphCanvas.replaceChildren(element('p','subtle',text('Rendering…')));try{if(!window.mermaid)throw new Error('Mermaid renderer unavailable');if(!window.__relayMermaidReady){window.mermaid.initialize({startOnLoad:false,securityLevel:'strict',flowchart:{htmlLabels:false}});window.__relayMermaidReady=true;}const rendered=await window.mermaid.render('relay-graph-'+graph.id.replace(/[^A-Za-z0-9_-]/g,'-'),graph.source);if(graph.id!==selectedGraphId)return;const svg=sanitizeGraphSvg(rendered.svg);graphCanvas.replaceChildren(svg);if(graph.id!=='entry'){const events=[...new Set([...graph.source.matchAll(/\\|on:\\s*([A-Za-z0-9_.-]+)\\|/g)].map(m=>m[1]))];const links=document.querySelector('#graph-links');for(const event of events){const button=element('button','refresh-button',text('Event: '+event));button.addEventListener('click',()=>graphDetail('Event',event));links.append(button);}}const definitions=graphNodeDefinitions(graph.source);for(const node of svg.querySelectorAll('g.node')){const entry=Array.from(definitions.entries()).find(([key])=>node.id.includes('-flowchart-'+key+'-'));const label=entry?.[1]||'';if(label)addNodeLabel(node,label);if(graph.id==='entry'){const target=Object.keys(GRAPH_NAV).find(key=>node.id.includes('-flowchart-'+key+'-'));if(target){const next=graphTargets(target).find(id=>workflowGraphs.some(g=>g.id===id));if(next)interactiveNode(node,()=>navigateGraph(next),'Explore '+target);}}else{const match=/^(state|guard|action):\\s*([A-Za-z0-9_.-]+)/.exec(label);if(match)interactiveNode(node,()=>graphDetail(match[1]==='guard'?'Guard':match[1]==='action'?'Action':'State',match[2]),'Inspect '+match[1]+' '+match[2]);}}applyGraphPalette(svg);}catch(error){graphCanvas.replaceChildren(element('p','error-state',text('Visual preview unavailable: '+error.message+'. Mermaid source remains available below.')));}}
318
+ async function drawGraphs(){try{const graphs=await loadJSON('/api/graphs');workflowGraphs=[{id:'entry',name:'Entry / Happy Path',path:'Dashboard-only navigation (not executable)',source:GRAPH_ENTRY_SOURCE},...(Array.isArray(graphs)?graphs:[])];const requested=graphFromHash();selectedGraphId=requested||'entry';graphsMeta.textContent=(workflowGraphs.length-1)+' executable workflow graphs · design view';renderGraphList();await renderSelectedGraph();}catch(error){graphsMeta.textContent='Unavailable';workflowGraphs=[{id:'entry',name:'Entry / Happy Path',path:'Dashboard-only navigation',source:GRAPH_ENTRY_SOURCE}];selectedGraphId='entry';renderGraphList();await renderSelectedGraph();graphPath.textContent='Executable graphs unavailable: '+error.message;}}
455
319
  function renderAudit(records){auditLog.replaceChildren();if(!Array.isArray(records)||!records.length){auditLog.append(element('p','empty',text('No schema v5 routing decisions yet.')));return;}for(const record of records){const card=element('article','audit-entry');const head=element('div','audit-entry-head');head.append(element('strong','',text(routeWorker(record)+' · '+value(record?.selected?.model?.model))),element('span','subtle',text(new Date(record.timestamp).toLocaleString())));card.append(head);const correlation=record.correlation||{};card.append(element('div','subtle',text([correlation.job_id,correlation.task_id].filter(Boolean).join(' / ')||'uncorrelated')));const probabilities=record.decision_evidence?.probabilities||{};const gates=(record.gate_results||[]).filter(item=>!item.eligible).map(item=>item.candidate_id+': '+item.rejection_reasons.join(', '));const nl=String.fromCharCode(10);const details=document.createElement('pre');details.textContent='fallback: '+String(Boolean(record.decision_evidence?.fallback))+(Object.keys(probabilities).length?nl+'probabilities: '+JSON.stringify(probabilities,null,2):'')+(gates.length?nl+'rejected: '+gates.join(nl):'');card.append(details);auditLog.append(card);}}
456
320
  async function drawAudit(){auditRefresh.disabled=true;try{renderAudit(await loadJSON('/api/decision-audit'));}catch(error){auditLog.replaceChildren(element('p','error-state',text('Audit unavailable: '+error.message)));}finally{auditRefresh.disabled=false;}}
457
321
  async function drawRules(){rulesStatus.textContent='';try{const rules=await loadJSON('/api/execution-router-rules');rulesText.value=typeof rules.text==='string'?rules.text:'';rulesMeta.textContent=rules.hash&&rules.hash!=='none'?'Loaded · '+rules.hash.slice(0,12):'No custom rules saved';}catch(error){rulesMeta.textContent='Unavailable';rulesStatus.textContent='Could not load rules: '+error.message;}void drawAudit();}
@@ -473,14 +337,20 @@ mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJo
473
337
  mobileJobs.addEventListener('change',syncMobileJobsView);
474
338
  window.addEventListener('hashchange',syncMobileJobsView);
475
339
  usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
476
- graphsTab.addEventListener('click',()=>{location.hash='#/graphs';showView('graphs');});
340
+ graphsTab.addEventListener('click',()=>{location.hash='#/graphs/entry';showView('graphs');});
341
+ document.querySelector('#graph-entry').addEventListener('click',()=>navigateGraph('entry'));
342
+ const graphDialog=document.querySelector('#graph-dialog');const graphViewport=document.querySelector('#graph-dialog-viewport');let graphZoom=1;const graphCanvasHome=graphCanvas.parentNode;function setGraphZoom(value){graphZoom=Math.min(4,Math.max(.35,value));const svg=graphCanvas.querySelector('svg');if(svg){svg.style.width=(Number(svg.getAttribute('width'))||Math.max(900,svg.viewBox.baseVal.width))*graphZoom+'px';svg.style.maxWidth='none';}document.querySelector('#graph-zoom-level').textContent=Math.round(graphZoom*100)+'%';}document.querySelector('#graph-maximize').addEventListener('click',()=>{graphDialog.showModal();graphViewport.append(graphCanvas);document.querySelector('#graph-dialog-title').textContent=graphTitle.textContent;setGraphZoom(1);});function closeGraphViewer(){graphCanvasHome.insertBefore(graphCanvas,graphCanvasHome.querySelector('.graph-source-details'));setGraphZoom(1);}graphDialog.addEventListener('close',closeGraphViewer);document.querySelector('#graph-dialog-close').addEventListener('click',()=>graphDialog.close());document.querySelector('#graph-zoom-in').addEventListener('click',()=>setGraphZoom(graphZoom*1.25));document.querySelector('#graph-zoom-out').addEventListener('click',()=>setGraphZoom(graphZoom/1.25));document.querySelector('#graph-zoom-reset').addEventListener('click',()=>setGraphZoom(1));graphViewport.addEventListener('wheel',event=>{if(!event.ctrlKey&&!event.metaKey)return;event.preventDefault();setGraphZoom(graphZoom*(event.deltaY<0?1.15:1/1.15));},{passive:false});let drag=null;graphViewport.addEventListener('pointerdown',event=>{if(event.target.closest('.drillable'))return;drag={x:event.clientX,y:event.clientY,left:graphViewport.scrollLeft,top:graphViewport.scrollTop};graphViewport.setPointerCapture(event.pointerId);});graphViewport.addEventListener('pointermove',event=>{if(!drag)return;graphViewport.scrollLeft=drag.left-(event.clientX-drag.x);graphViewport.scrollTop=drag.top-(event.clientY-drag.y);});graphViewport.addEventListener('pointerup',()=>drag=null);graphViewport.addEventListener('pointercancel',()=>drag=null);
343
+ document.querySelector('#graph-back').addEventListener('click',()=>history.back());
344
+ document.querySelector('#graph-forward').addEventListener('click',()=>history.forward());
345
+ window.addEventListener('popstate',()=>{if(graphFromHash()){renderGraphNavigation();showView('graphs');}else if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');});
346
+ window.addEventListener('hashchange',()=>{if(graphFromHash()){renderGraphNavigation();showView('graphs');}else if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');});
477
347
  rulesTab.addEventListener('click',()=>{location.hash='#/execution-router-rules';showView('rules');});
478
348
  shortcutsTab.addEventListener('click',()=>{location.hash='#/shortcuts';showView('shortcuts');});
479
349
  rulesSave.addEventListener('click',()=>void saveRules());
480
350
  rulesTest.addEventListener('click',()=>void testRouter());
481
351
  auditRefresh.addEventListener('click',()=>void drawAudit());
482
352
  shortcutsSave.addEventListener('click',saveShortcuts);
483
- window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','shortcuts'];const current=location.hash==='#/usage'?'usage':location.hash==='#/graphs'?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':'jobs';const next=views[(views.indexOf(current)+1)%views.length];location.hash=next==='usage'?'#/usage':next==='graphs'?'#/graphs':next==='rules'?'#/execution-router-rules':next==='shortcuts'?'#/shortcuts':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
353
+ window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();const views=['jobs','usage','graphs','rules','shortcuts'];const current=location.hash==='#/usage'?'usage':graphFromHash()?'graphs':location.hash==='#/execution-router-rules'?'rules':location.hash==='#/shortcuts'?'shortcuts':'jobs';const next=views[(views.indexOf(current)+1)%views.length];location.hash=next==='usage'?'#/usage':next==='graphs'?'#/graphs':next==='rules'?'#/execution-router-rules':next==='shortcuts'?'#/shortcuts':(selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs');showView(next);}});
484
354
  function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
485
355
  function isFreshEnough(incoming,current){const a=Date.parse(incoming||''),b=Date.parse(current||'');return !Number.isFinite(b)||!Number.isFinite(a)||a>=b;}
486
356
  function applyStatePatch(patch){if(!patch?.jobId)return false;const job=jobs.find(candidate=>candidate.id===patch.jobId);if(!job)return false;if(patch.jobState&&isFreshEnough(patch.jobUpdatedAt,job.updatedAt)){job['state']=patch.jobState;if(patch.jobUpdatedAt)job.updatedAt=patch.jobUpdatedAt;}if(patch.taskId&&patch.taskState){const task=(job.tasks||[]).find(candidate=>candidate.id===patch.taskId);if(task&&isFreshEnough(patch.taskUpdatedAt,task.updatedAt)){task['state']=patch.taskState;if(patch.taskUpdatedAt)task.updatedAt=patch.taskUpdatedAt;}}return true;}
@@ -490,7 +360,7 @@ stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label
490
360
  stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
491
361
  stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){upsertFreshJob(payload.job);renderAll();void drawRuntimeSummary();}};
492
362
  stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job)upsertFreshJob(payload.job);else if(payload?.statePatch)applyStatePatch(payload.statePatch);if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
493
- if(location.hash==='#/usage')showView('usage');else if(location.hash==='#/graphs')showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
363
+ if(location.hash==='#/usage')showView('usage');else if(graphFromHash())showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else showView('jobs');
494
364
  setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
495
365
  setInterval(()=>void refreshVersion(),30000);
496
366
  async function refreshSelected(){const job=selected();if(!job){await refresh();return;}selectedJobRefresh.disabled=true;selectedJobRefresh.textContent='Refreshing…';try{const params=new URLSearchParams({id:job.id,repo:job.repository,pr:String(job.prNumber)});const updated=await loadJSON('/api/job?'+params.toString());const index=jobs.findIndex(candidate=>jobSelectionKey(candidate)===jobSelectionKey(job));if(index>=0)jobs[index]=updated;else jobs.push(updated);selectedJob=jobSelectionKey(updated);rememberSelection();renderAll();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';}finally{selectedJobRefresh.disabled=false;selectedJobRefresh.textContent='Refresh';}}
@@ -0,0 +1,186 @@
1
+ export const dashboardStyles = ` <style>
2
+ :root { color-scheme: light dark; --bg:#f3f5f8; --surface:#fff; --surface2:#f8fafc; --border:#d8dee7; --text:#17202b; --muted:#687587; --accent:#285f86; --accent2:#e8f1f8; --ok:#19703a; --warn:#8b5b08; --bad:#a33c3c; --shadow:0 1px 2px rgba(15,23,42,.06); }
3
+ * { box-sizing:border-box; }
4
+ [hidden] { display:none !important; }
5
+ body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,sans-serif; }
6
+ header { position:sticky; top:0; z-index:3; background:var(--surface); border-bottom:1px solid var(--border); padding:.8rem 1rem; }
7
+ .brand { display:flex; align-items:baseline; gap:.65rem; min-width:0; }
8
+ .version { margin-left:auto; color:var(--muted); font-size:.72rem; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; white-space:nowrap; }
9
+ .runtime-summary { display:flex; gap:.4rem; align-items:center; flex-wrap:wrap; margin-top:.55rem; }
10
+ .runtime-stat { padding:.18rem .45rem; border:1px solid var(--border); border-radius:999px; background:var(--surface2); color:var(--muted); font-size:.72rem; white-space:nowrap; }
11
+ .runtime-stat strong { color:var(--text); }
12
+ h1 { margin:0; font-size:1.08rem; letter-spacing:-.02em; }
13
+ .tagline,.subtle { color:var(--muted); font-size:.78rem; }
14
+ .warning-banner { margin:.65rem 0 0; padding:.5rem .65rem; border:1px solid #e6bf62; border-radius:.4rem; background:#fff4d6; color:#6f4b00; font-size:.78rem; font-weight:700; }
15
+ .tagline { margin:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
16
+ nav[role="tablist"] { display:flex; gap:.3rem; margin-top:.65rem; }
17
+ nav button { border:0; border-radius:.35rem; background:transparent; color:var(--muted); padding:.38rem .68rem; font:inherit; font-weight:700; cursor:pointer; }
18
+ nav button[aria-selected="true"] { background:var(--accent); color:#fff; }
19
+ button:focus-visible,a:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
20
+ main { max-width:1500px; margin:auto; padding:1rem; display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:1rem; align-items:start; }
21
+ .panel { min-width:0; background:var(--surface); border:1px solid var(--border); border-radius:.55rem; box-shadow:var(--shadow); }
22
+ .panel-head { display:flex; align-items:center; justify-content:space-between; gap:.6rem; padding:.65rem .75rem; border-bottom:1px solid var(--border); }
23
+ .panel-head h2,.panel-head h3 { margin:0; color:var(--muted); font-size:.75rem; letter-spacing:.07em; text-transform:uppercase; min-width:0; }\n #workspace-heading { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n #selected-job-title { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
24
+ .panel-actions { display:flex; align-items:center; gap:.45rem; }
25
+ .job-filters { display:flex; gap:.35rem; align-items:center; flex-wrap:wrap; }
26
+ .job-filter { max-width:145px; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--text); padding:.27rem .4rem; font:inherit; font-size:.72rem; }
27
+ .job-search { width:95px; max-width:14vw; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--text); padding:.27rem .45rem; font:inherit; font-size:.72rem; }
28
+ .quick-filter { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.27rem .42rem; font:inherit; font-size:.7rem; font-weight:750; cursor:pointer; }
29
+ .quick-filter[aria-pressed="true"] { border-color:var(--accent); background:var(--accent2); color:var(--accent); }
30
+ .refresh-button { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.28rem .55rem; font:inherit; font-size:.72rem; font-weight:700; cursor:pointer; }
31
+ .refresh-button:disabled { cursor:default; opacity:.55; }
32
+ .mobile-only { display:none; }
33
+ .panel-body { padding:.75rem; overflow:auto; max-height:calc(100vh - 11.5rem); }
34
+ #jobs-workspace > .panel:first-child .panel-body { overflow:visible; max-height:none; }
35
+ .jobs-panel { grid-row:1/span 2; }
36
+ .workspace { display:grid; gap:1rem; }
37
+ .job-list,.tree,.event-list { list-style:none; margin:0; padding:0; }
38
+ .job-list { display:grid; gap:.35rem; }
39
+ .job-row { display:grid; gap:.2rem; padding:.35rem .45rem; border:1px solid transparent; border-radius:.4rem; cursor:pointer; }
40
+ .job-row[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
41
+ .job-row.complete-unmerged { border-color:#d69a1b; background:#fff8df; }
42
+ .job-row.complete-unmerged[aria-current="true"] { border-color:#b77900; background:#fff0bd; }
43
+ .job-content { min-width:0; display:grid; gap:.2rem; }
44
+ .job-button { min-width:0; display:block; padding:.12rem 0; border:0; background:transparent; color:inherit; text-align:left; font:inherit; cursor:pointer; }
45
+ .job-title { font-weight:750; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
46
+ .job-last-line { display:flex; align-items:center; justify-content:space-between; gap:.5rem; min-width:0; }
47
+ .job-last-line .subtle:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
48
+ .job-last-line .subtle:last-child { flex:none; white-space:nowrap; }
49
+ .job-primary { display:flex; align-items:center; justify-content:space-between; gap:.45rem; min-width:0; }
50
+ .job-status-line { display:flex; align-items:center; gap:.3rem; min-width:0; white-space:nowrap; }
51
+ .job-status-line .badge { flex:0 0 auto; }
52
+ .status-pill,.badge { display:inline-flex; align-items:center; border-radius:999px; padding:.06rem .42rem; min-height:1.35rem; font-size:.68rem; font-weight:800; }
53
+ .status-pill.ok,.badge.ACTIVE,.badge.RUNNING,.badge.SUCCEEDED,.badge.COMPLETED,.badge.MERGED { background:#e5f3e9; color:var(--ok); }
54
+ .status-pill.warn,.badge.BLOCKED,.badge.DRAFT,.badge.QUEUED,.badge.WAITING,.badge.OPEN { background:#fff1d2; color:var(--warn); }
55
+ .status-pill.bad,.badge.FAILED { background:#ffe8e8; color:var(--bad); }
56
+ .status-pill.neutral,.badge.IDLE,.badge.CANCELLED { background:#edf0f4; color:var(--muted); }
57
+ .badge.DRAFT { background:#eef2ff; color:#3f51b5; }
58
+ .badge.MERGE_PENDING { background:#ffe3a3; color:#7a4a00; }
59
+ .details-dialog { width:min(850px,calc(100vw - 2rem)); max-height:85vh; overflow:auto; border:1px solid var(--border); border-radius:.6rem; background:var(--surface); color:var(--text); box-shadow:0 18px 55px #0004; padding:1rem; } .details-dialog::backdrop { background:#17202b99; } .details-dialog-head { display:flex; justify-content:space-between; align-items:center; gap:1rem; position:sticky; top:-1rem; background:var(--surface); padding:.5rem 0; } .details-dialog pre { white-space:pre-wrap; overflow-wrap:anywhere; background:var(--surface2); padding:.7rem; border-radius:.4rem; } .details-dialog h3 { font-size:.8rem; margin:1rem 0 .3rem; }
60
+ .job-summary,.detail-groups { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.55rem; }
61
+ .detail-group { min-width:0; padding:.5rem; background:var(--surface2); border:1px solid var(--border); border-radius:.4rem; }
62
+ .detail-label { margin:0; color:var(--muted); font-size:.66rem; text-transform:uppercase; letter-spacing:.04em; }
63
+ .detail-value { margin:.12rem 0 0; font-weight:650; overflow-wrap:anywhere; }
64
+ .section-title { margin:.8rem 0 .35rem; color:var(--muted); font-size:.72rem; letter-spacing:.05em; text-transform:uppercase; }
65
+ ul.tree { display:grid; gap:.25rem; min-width:0; max-width:100%; }
66
+ ul.tree li { min-width:0; max-width:100%; }
67
+ ul.tree ul { width:calc(100% - .6rem); min-width:0; max-width:calc(100% - .6rem); list-style:none; margin:.2rem 0 .1rem .6rem; padding-left:.6rem; border-left:1px solid var(--border); }
68
+ .task-line { display:flex; align-items:center; gap:.2rem; min-width:0; }
69
+ .task-fold { flex:0 0 auto; width:1.35rem; padding:.32rem 0; border:0; background:transparent; color:var(--muted); font:inherit; cursor:pointer; }
70
+ .task-row { width:100%; max-width:100%; overflow:hidden; flex:1 1 0; display:flex; align-items:center; gap:.4rem; min-width:0; padding:.32rem .38rem; border:0; border-radius:.35rem; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
71
+ .task-row[aria-current="true"] { background:var(--accent2); }
72
+ .task-row .badge { flex:0 0 auto; }
73
+ .task-id { min-width:0; max-width:100%; flex:1 1 0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font: .77rem ui-monospace,SFMono-Regular,Menlo,monospace; }
74
+ .task-actions { display:flex; justify-content:flex-end; gap:.25rem; margin-bottom:.5rem; }
75
+ .task-link { color:var(--accent); font-weight:700; }
76
+ .agent-graph { display:grid; gap:.45rem; margin-top:.55rem; padding:.55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); overflow:auto; }
77
+ .agent-wave { display:flex; align-items:center; gap:.4rem; min-width:max-content; }
78
+ .agent-node { padding:.32rem .48rem; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); font:.73rem ui-monospace,SFMono-Regular,Menlo,monospace; }
79
+ .agent-arrow { color:var(--muted); font-weight:800; }
80
+ .agent-graph-source { margin:.1rem 0 0; padding:.45rem; border-radius:.35rem; background:var(--surface); color:var(--muted); font:.7rem/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; overflow:auto; }
81
+ .event-list { display:grid; gap:.45rem; }
82
+ .event-item { padding:.34rem .42rem .42rem; border-left:3px solid transparent; border-bottom:1px solid var(--border); border-radius:.28rem; }
83
+ .event-item.event-error { border-left-color:var(--bad); background:#fff4f4; }
84
+ .event-item.event-error .event-kind,.event-item.event-error .event-message { color:var(--bad); }
85
+ .event-item.event-warn { border-left-color:var(--warn); background:#fff9eb; }
86
+ .event-item.event-warn .event-kind { color:var(--warn); }
87
+ .event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
88
+ .event-kind { color:var(--accent); font-weight:800; }
89
+ .event-message { margin:.12rem 0 0; overflow-wrap:anywhere; }
90
+ #usage-view,#graphs-view,#rules-view,#shortcuts-view,#worker-rules-view { grid-column:1 / -1; }
91
+ .graphs-browser { display:grid; grid-template-columns:minmax(190px,260px) minmax(0,1fr); gap:.75rem; min-height:60vh; }
92
+ .graph-list { display:grid; align-content:start; gap:.35rem; }
93
+ .graph-button { width:100%; display:grid; gap:.08rem; padding:.48rem .55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--text); text-align:left; font:inherit; cursor:pointer; }
94
+ .graph-button[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
95
+ .graph-button strong { font-size:.78rem; }
96
+ .graph-button span { color:var(--muted); font:.66rem ui-monospace,SFMono-Regular,Menlo,monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
97
+ .graph-preview { min-width:0; display:grid; align-content:start; gap:.55rem; }
98
+ .graph-preview-head { display:flex; justify-content:space-between; gap:.7rem; align-items:baseline; flex-wrap:wrap; }
99
+ .graph-preview-head h3 { margin:0; font-size:.95rem; }
100
+ .graph-canvas { min-height:24rem; display:grid; place-items:center; overflow:auto; padding:1rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
101
+ .graph-canvas svg { max-width:none; height:auto; }
102
+ .graph-navigation { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; margin-bottom:.7rem; }
103
+ .graph-canvas .node.drillable { cursor:pointer; }
104
+ .graph-dialog { width:96vw; height:94vh; max-width:none; max-height:none; padding:.7rem; border:1px solid var(--border); border-radius:.5rem; background:var(--surface); color:var(--text); }
105
+ .graph-dialog::backdrop { background:#17202b99; }
106
+ .graph-dialog-header { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.7rem; padding:.3rem .5rem; }
107
+ .graph-dialog-viewport { height:calc(94vh - 5rem); overflow:auto; cursor:grab; background:var(--surface2); border:1px solid var(--border); border-radius:.4rem; }
108
+ .graph-dialog-viewport:active { cursor:grabbing; }
109
+ .graph-dialog-viewport .graph-canvas { display:block; width:max-content; min-width:100%; min-height:100%; overflow:visible; }
110
+ .graph-canvas .node text { pointer-events:none; }
111
+ .graph-source { margin:0; max-height:22rem; overflow:auto; padding:.7rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); color:var(--muted); font:.72rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; white-space:pre; }
112
+ .graph-source-details summary { cursor:pointer; color:var(--muted); font-size:.74rem; font-weight:700; }
113
+ .rules-editor { display:grid; gap:.65rem; }
114
+ .rules-help { margin:0; color:var(--muted); font-size:.8rem; }
115
+ .rules-textarea { width:100%; min-height:55vh; resize:vertical; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); color:var(--text); padding:.8rem; font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; }
116
+ .rules-footer { display:flex; align-items:center; justify-content:space-between; gap:.75rem; flex-wrap:wrap; }
117
+ .rules-save { border:1px solid var(--accent); border-radius:.35rem; background:var(--accent); color:#fff; padding:.38rem .8rem; font:inherit; font-weight:750; cursor:pointer; }
118
+ .rules-save:disabled { opacity:.55; cursor:default; }
119
+ .usage-providers { display:grid; gap:.55rem; }
120
+ .usage-provider { display:grid; grid-template-columns:minmax(130px,180px) minmax(0,1fr); gap:1rem; align-items:center; padding:.7rem .8rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface2); }
121
+ .usage-provider h3 { margin:0; font-size:.86rem; }
122
+ .usage-bars { display:grid; gap:1rem; }
123
+ .usage-row { display:grid; gap:.35rem; }
124
+ .usage-label { display:flex; justify-content:space-between; gap:1rem; font-weight:700; }
125
+ .usage-track { height:.72rem; overflow:hidden; border:1px solid var(--border); border-radius:999px; background:var(--surface2); }
126
+ .usage-fill { height:100%; width:0; border-radius:inherit; background:var(--accent); transition:width .2s ease; }
127
+ .usage-details { border-top:1px solid var(--border); padding-top:.45rem; }
128
+ .usage-details summary { cursor:pointer; color:var(--muted); font-size:.72rem; font-weight:700; }
129
+ .usage-details-body { display:grid; gap:.3rem; margin-top:.4rem; overflow-wrap:anywhere; }
130
+ .empty,.error-state { padding:.85rem; border:1px dashed var(--border); border-radius:.4rem; color:var(--muted); }
131
+ .error-state,.error-text { color:var(--bad); }
132
+ .skeleton { height:3.2rem; border-radius:.4rem; background:linear-gradient(90deg,#edf0f4,#fafbfc,#edf0f4); animation:pulse 1.2s infinite; }
133
+ @keyframes pulse { 50% { opacity:.55; } }
134
+ @media (max-width:1050px) { main { grid-template-columns:1fr; } .jobs-panel { grid-row:auto; } .panel-body { max-height:24rem; } #jobs-workspace > .panel:first-child .panel-body { max-height:none; } }
135
+ @media (max-width:650px) {
136
+ header { padding:.65rem .7rem; }
137
+ nav[role="tablist"] { overflow-x:auto; padding-bottom:.1rem; }
138
+ nav button { flex:0 0 auto; white-space:nowrap; }
139
+ main { padding:.5rem; gap:.65rem; }
140
+ body[data-mobile-jobs-view="list"] #jobs-workspace { display:none; }
141
+ body[data-mobile-jobs-view="detail"] #jobs-view { display:none; }
142
+ .mobile-only { display:inline-flex; }
143
+ #jobs-view .panel-head { display:grid; align-items:start; }
144
+ #jobs-view .panel-actions { width:100%; flex-wrap:wrap; }
145
+ .runtime-summary { display:none; }
146
+ #jobs-view .job-filters { width:auto; }
147
+ #jobs-view .job-search,
148
+ #jobs-view #quick-status,
149
+ #jobs-view #quick-autonomous,
150
+ #jobs-view #quick-draft { display:none; }
151
+ #jobs-view .panel-body { max-height:none; }
152
+ #jobs-workspace { gap:.65rem; }
153
+ #jobs-workspace .panel-body { max-height:none; }
154
+ #usage-view .panel-head { padding:.6rem .7rem .35rem; }
155
+ #usage-view .panel-head .subtle { display:none; }
156
+ .graphs-browser { grid-template-columns:1fr; min-height:0; }
157
+ .graph-list { grid-template-columns:repeat(2,minmax(0,1fr)); }
158
+ .graph-canvas { min-height:18rem; }
159
+ #usage-view .panel-body { padding:.45rem .5rem .65rem; max-height:none; }
160
+ #usage-view .usage-providers { gap:.45rem; }
161
+ #usage-view .usage-provider {
162
+ grid-template-columns:1fr;
163
+ gap:.55rem;
164
+ padding:.65rem .7rem;
165
+ border-radius:.55rem;
166
+ background:var(--surface);
167
+ }
168
+ #usage-view .usage-provider h3 { font-size:.78rem; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; }
169
+ #usage-view .usage-bars { gap:.7rem; }
170
+ #usage-view .usage-row { gap:.25rem; }
171
+ #usage-view .usage-label { align-items:baseline; gap:.5rem; font-size:.78rem; }
172
+ #usage-view .usage-label span:last-child { font-size:1.05rem; font-weight:800; color:var(--text); }
173
+ #usage-view .usage-track { height:.48rem; border:0; background:var(--border); }
174
+ #usage-view .usage-row .subtle { font-size:.68rem; line-height:1.25; }
175
+ #usage-view .usage-provider > .detail-value { font-size:1.2rem; font-weight:800; }
176
+ #usage-view .usage-details { padding-top:.35rem; }
177
+ #usage-view .usage-details-body { font-size:.68rem; }
178
+ .details-dialog { width:min(850px,calc(100vw - 2rem)); max-height:85vh; overflow:auto; border:1px solid var(--border); border-radius:.6rem; background:var(--surface); color:var(--text); box-shadow:0 18px 55px #0004; padding:1rem; } .details-dialog::backdrop { background:#17202b99; } .details-dialog-head { display:flex; justify-content:space-between; align-items:center; gap:1rem; position:sticky; top:-1rem; background:var(--surface); padding:.5rem 0; } .details-dialog pre { white-space:pre-wrap; overflow-wrap:anywhere; background:var(--surface2); padding:.7rem; border-radius:.4rem; } .details-dialog h3 { font-size:.8rem; margin:1rem 0 .3rem; }
179
+ .job-summary,.detail-groups { grid-template-columns:1fr; }
180
+ .panel-head { padding:.55rem .6rem; }
181
+ .panel-body { padding:.6rem; }
182
+ .tagline { display:none; }
183
+ }
184
+ @media (prefers-reduced-motion:reduce) { .skeleton { animation:none; } .usage-fill { transition:none; } }
185
+ .router-rules-workspace{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;align-items:start;min-width:0}.router-rules-pane{min-width:0;max-width:100%;overflow:hidden}.rules-editor .rules-textarea{min-height:420px}.audit-log{display:flex;flex-direction:column;gap:8px;max-height:500px;overflow:auto;min-width:0}.audit-entry{border:1px solid var(--border);border-radius:10px;padding:10px;background:var(--surface)}.audit-entry-head{display:flex;justify-content:space-between;gap:8px;align-items:center}.audit-entry pre{white-space:pre-wrap;word-break:break-word;margin:8px 0 0;font-size:11px}.router-test-panel{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}.router-test-results{margin-top:8px;display:flex;flex-direction:column;gap:6px;max-height:480px;overflow:auto;min-width:0}.router-test-results pre{white-space:pre;overflow:auto;max-width:100%;font-size:11px;line-height:1.45;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px}.router-test-row{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:7px 0;border-bottom:1px solid var(--border)}@media(max-width:900px){.router-rules-workspace{grid-template-columns:1fr}.router-rules-pane{max-height:none}.router-test-results{max-height:420px}}@media(max-width:650px){#rules-view .panel-head .subtle{display:none}#rules-view .panel-body{max-height:none;overflow:visible;padding:.6rem}#rules-view .router-rules-workspace{gap:.9rem}#rules-view .rules-help{font-size:.74rem;line-height:1.4}#rules-view .rules-textarea{min-height:52vh;padding:.65rem;font-size:12px}#rules-view .rules-footer{align-items:stretch}#rules-view .rules-footer>.subtle{width:100%}#rules-view .rules-footer>div{width:100%;display:grid;grid-template-columns:1fr 1fr;gap:.5rem}#rules-view .rules-save{min-height:44px;width:100%;padding:.55rem .7rem}#rules-view .router-rules-pane>.panel-head{padding:.55rem 0}#rules-view .audit-log{max-height:none}#rules-view .audit-entry-head{align-items:flex-start}#rules-view .audit-entry-head .subtle{display:block;flex:0 0 auto}#rules-view .router-test-panel{min-width:0;max-width:100%;overflow:hidden}#rules-view .router-test-results{max-height:none;width:100%;max-width:100%;overflow-x:hidden}#rules-view .router-test-results>*{min-width:0;max-width:100%}#rules-view .router-test-results pre{width:100%;box-sizing:border-box;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:11px;padding:.65rem}#rules-view .router-test-row{grid-template-columns:1fr}}
186
+ </style>`;
@@ -0,0 +1,84 @@
1
+ /** Read-only code evidence for diagram symbols. Never controls a workflow transition. */
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const ROOT = fileURLToPath(new URL('../', import.meta.url));
6
+ const NAME = /^[A-Za-z0-9_.-]{1,100}$/;
7
+ const KINDS = new Set(['Event', 'Guard', 'Action', 'State']);
8
+ async function sourceFiles(relative) {
9
+ const directory = join(ROOT, relative);
10
+ let entries;
11
+ try {
12
+ entries = await readdir(directory, { withFileTypes: true });
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT')
16
+ return [];
17
+ throw error;
18
+ }
19
+ const paths = [];
20
+ for (const entry of entries) {
21
+ const child = `${relative}/${entry.name}`;
22
+ if (entry.isDirectory())
23
+ paths.push(...await sourceFiles(child));
24
+ else if (entry.isFile() && /\.(?:ts|js|mjs|md)$/.test(entry.name))
25
+ paths.push(child);
26
+ }
27
+ return paths;
28
+ }
29
+ function matches(content, symbol, path, limit) {
30
+ const result = [];
31
+ for (const [index, line] of content.split('\n').entries()) {
32
+ if (new RegExp('(^|[^A-Za-z0-9_.-])' + symbol.replaceAll('.', '\\.') + '($|[^A-Za-z0-9_.-])').test(line))
33
+ result.push({ path, line: index + 1, evidence: 'text-search' });
34
+ if (result.length >= limit)
35
+ break;
36
+ }
37
+ return result;
38
+ }
39
+ async function findReferences(paths, symbol, max) {
40
+ const results = [];
41
+ for (const path of paths) {
42
+ const content = await readFile(join(ROOT, path), 'utf8');
43
+ results.push(...matches(content, symbol, path, max - results.length));
44
+ if (results.length >= max)
45
+ break;
46
+ }
47
+ return results;
48
+ }
49
+ export async function inspectWorkflowSymbol(graphId, kind, name) {
50
+ if (!NAME.test(graphId) || !NAME.test(name) || !KINDS.has(kind))
51
+ return null;
52
+ const graphPath = `workflow/${graphId}.mmd`;
53
+ let source;
54
+ try {
55
+ source = await readFile(join(ROOT, graphPath), 'utf8');
56
+ }
57
+ catch (error) {
58
+ if (error.code === 'ENOENT')
59
+ return null;
60
+ throw error;
61
+ }
62
+ const escaped = name.replaceAll('.', '\\.');
63
+ const pattern = kind === 'Event' ? new RegExp(`\\|on:\\s*${escaped}\\|`) : new RegExp(`\\["${kind.toLowerCase()}:\\s*${escaped}"\\]`);
64
+ const matchingLine = source.split('\n').findIndex(line => pattern.test(line));
65
+ if (matchingLine < 0)
66
+ return null;
67
+ const contracts = ['skills/agents-relay/contracts/event-contract.md', 'skills/agents-relay/contracts/task-recovery-flows.md'];
68
+ const contractRefs = (await Promise.all(contracts.map(async (path) => {
69
+ try {
70
+ return matches(await readFile(join(ROOT, path), 'utf8'), name, path, 2);
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ }))).flat();
76
+ const sourcePaths = (await sourceFiles('src')).sort((a, b) => Number(b.startsWith('src/workflow/')) - Number(a.startsWith('src/workflow/')));
77
+ const testPaths = await sourceFiles('test');
78
+ return {
79
+ graph: { path: graphPath, line: matchingLine + 1, evidence: "exact-token" },
80
+ contracts: contractRefs,
81
+ implementation: await findReferences(sourcePaths, name, 8),
82
+ tests: await findReferences(testPaths, name, 8),
83
+ };
84
+ }
@@ -0,0 +1,22 @@
1
+ /** Conceptual dashboard-only overview; workflow/*.mmd remains the transition authority. */
2
+ export const workflowPresentation = {
3
+ entrySource: [
4
+ 'flowchart LR',
5
+ 'objective["START: Job Objective"] --> planner["Planner"] --> worker["Worker"] --> reviewer["Reviewer"] --> merge["Merge / Delivery"] --> achieved["Objective Achieved"] --> completed["Job Completed"]',
6
+ 'reviewer -->|Changes Required| fix["Fix Task"] --> worker',
7
+ 'planner -->|Failed| troubleshooter["Troubleshooter"]',
8
+ 'worker -->|Failed| troubleshooter',
9
+ 'reviewer -->|Failed| troubleshooter',
10
+ 'merge -->|Failed| troubleshooter',
11
+ 'troubleshooter --> resolution["Resolution"]',
12
+ 'resolution -->|Recovered| planner',
13
+ 'resolution -->|Recovered| worker',
14
+ 'resolution -->|Recovered| reviewer',
15
+ 'resolution -->|Recovered| merge',
16
+ 'resolution -->|Needs human action| escalator["Escalator workflow"] -->|Resolved| resolution',
17
+ 'classDef failure fill:#ffe4e4,stroke:#be3030,color:#7c1818',
18
+ 'class troubleshooter,resolution,escalator failure',
19
+ 'linkStyle 8,9,10,11,12,13,14,15,16,17,18 stroke:#be3030,stroke-width:2px'
20
+ ].join('\n'),
21
+ navigation: { objective: ['job-lifecycle'], planner: ['planner-orchestration', 'planner-trigger'], worker: ['task-lifecycle'], reviewer: ['review-lifecycle'], merge: ['job-lifecycle'], achieved: ['job-lifecycle'], completed: ['job-lifecycle'], fix: ['review-lifecycle', 'task-lifecycle'], troubleshooter: ['troubleshooter', 'task-resolution'], resolution: ['blocker-resolution', 'task-resolution', 'authoritative-unblock'], escalator: ['escalation'] }
22
+ };
package/dist/dashboard.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { inspectWorkflowSymbol } from './dashboard-workflow-inspection.js';
1
2
  import { createServer } from 'node:http';
2
3
  import { subscribeLocalCommits } from './local-commit-events.js';
3
4
  import { readFile, readdir } from 'node:fs/promises';
@@ -56,7 +57,7 @@ async function eventHistory(jobId, taskId, limit) {
56
57
  return [];
57
58
  }
58
59
  }
59
- export { dashboardHtml, dashboardVersion } from './dashboard-html.js';
60
+ export { dashboardHtml, dashboardVersion, dashboardSourceRevision } from './dashboard-html.js';
60
61
  import { dashboardHtml, dashboardVersion } from './dashboard-html.js';
61
62
  export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobId, usageRegistry, overviewProvider, webhook, statusProvider, githubUsageProvider, exactJobProvider, actions, routerTestProvider, decisionAuditProvider) {
62
63
  const legacyActions = typeof exactJobProvider === 'object' && exactJobProvider !== null ? exactJobProvider : undefined;
@@ -173,6 +174,19 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
173
174
  res.end(JSON.stringify({ version: dashboardVersion }));
174
175
  return;
175
176
  }
177
+ if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/graph-symbol' && req.method === 'GET') {
178
+ const query = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams;
179
+ try {
180
+ const result = await inspectWorkflowSymbol(query.get('graph') ?? '', query.get('kind') ?? '', query.get('name') ?? '');
181
+ res.writeHead(result ? 200 : 404, { 'content-type': 'application/json', 'cache-control': 'no-store' });
182
+ res.end(JSON.stringify(result ?? { error: 'No canonical graph symbol found' }));
183
+ }
184
+ catch (error) {
185
+ res.writeHead(500, { 'content-type': 'application/json' });
186
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Read error' }));
187
+ }
188
+ return;
189
+ }
176
190
  if (req.url === '/api/graphs') {
177
191
  try {
178
192
  const graphs = await dashboardWorkflowGraphs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.133",
3
+ "version": "2.0.135",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",