@pharaoh-so/mcp 0.3.51 → 0.3.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/trace-app.html CHANGED
@@ -6,60 +6,117 @@
6
6
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; img-src 'none'; font-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'">
7
7
  <title>Pharaoh Trace</title>
8
8
  <style>
9
- :root { color-scheme:dark; --bg:#171813; --text:#e5e6d8; --muted:#aaa995; --gold:#d4ad51; --yellow:#edc54c; --rule:#414236; --hover:#22251c; --error:#e3a18d; font:400 12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace; background:var(--bg); color:var(--text); }
10
- :root[data-theme="light"] { color-scheme:light; --bg:#faf8f1; --text:#292b20; --muted:#656553; --gold:#866315; --yellow:#926700; --rule:#d8d4c5; --hover:#efede2; --error:#a13929; }
9
+ :root { color-scheme:dark; --stone:#2a2520; --rule:#3a352e; --track:#342e28; --hover:#332d27; --gold:#d4a843; --papyrus:#e8dcc8; --text:#c4b99a; --muted:#9d927b; --red:#f87171; --amber:#f0c855; --green:#4ade80;
10
+ --s0:#d4a843; --s1:#b08a3a; --s2:#8c6e31; --s3:#6e5728; --s4:#574522; --s-muted:#5f574a; --s-warning:#f0c855; --s-error:#ef4444;
11
+ --sans:"DM Sans",system-ui,-apple-system,"Segoe UI",sans-serif; --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace; }
12
+ :root[data-theme="light"] { color-scheme:light; --stone:#f7f3ea; --rule:#e2d8c4; --track:#ece4d3; --hover:#efe8da; --gold:#946c0f; --papyrus:#2a2520; --text:#4a4235; --muted:#71675a; --red:#b42318; --amber:#9a5b00; --green:#15803d;
13
+ --s0:#b08515; --s1:#c9a24a; --s2:#d9bb72; --s3:#e6cf98; --s4:#efdfb8; --s-muted:#cfc4ad; --s-warning:#e0a526; --s-error:#d93f2f; }
11
14
  * { box-sizing:border-box; }
12
- body { margin:0; min-width:0; }
13
- #trace { border-left:2px solid var(--gold); padding:9px 14px 8px; min-width:0; }
14
- header { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:baseline; gap:3px 14px; color:var(--muted); font-size:10px; }
15
- #tool { overflow-wrap:anywhere; min-width:0; }
16
- #status { white-space:nowrap; }
17
- #status[data-state="ok"] { color:#4ade80; }
18
- #status[data-state="pending"],.filled { color:var(--yellow); }
19
- #status[data-state="error"] { color:var(--error); }
20
- #trace[data-pending="true"] #status { animation:pending 1.4s ease-in-out infinite alternate; }
21
- @keyframes pending { from { opacity:.55; } to { opacity:1; } }
22
- #repo { font-size:14px; margin:4px 0 0; overflow-wrap:anywhere; }
23
- #provenance { color:var(--muted); font-size:10px; overflow-wrap:anywhere; margin-top:2px; }
24
- h1 { font-size:14px; line-height:1.5; font-weight:500; margin:6px 0 0; overflow-wrap:anywhere; }
25
- #summary { margin:2px 0 0; color:var(--muted); overflow-wrap:anywhere; }
26
- ul { list-style:none; padding:0; margin:4px 0 0; min-width:0; }
27
- li { min-width:0; }
28
- summary,.row { display:grid; grid-template-columns:18px minmax(0,1fr) auto; gap:6px; align-items:baseline; padding:2px 0; min-width:0; }
29
- summary { list-style:none; min-height:24px; border-bottom:1px solid var(--rule); cursor:pointer; }
30
- summary::-webkit-details-marker { display:none; }
31
- summary:hover { background:var(--hover); }
32
- summary:focus-visible { outline:2px solid var(--gold); outline-offset:2px; }
33
- .branch { color:var(--gold); white-space:pre; }
34
- details>summary .branch::after { content:"+"; }
35
- details[open]>summary .branch::after { content:"−"; }
36
- .label,.detail,.badge { overflow-wrap:anywhere; white-space:pre-wrap; min-width:0; unicode-bidi:plaintext; }
37
- details[open]>summary .label { color:var(--gold); }
38
- .detail { display:block; color:var(--muted); font-size:10px; margin-top:1px; }
39
- .badge { color:var(--muted); font-size:10px; text-align:right; }
40
- .badge[data-state="ok"] { color:#4ade80; }
41
- .badge[data-state="warning"],.badge[data-state="pending"] { color:var(--yellow); }
42
- .badge[data-state="error"] { color:var(--error); }
43
- .children { margin:0 0 3px 17px; border-left:1px solid var(--rule); padding-left:9px; }
44
- .meter { display:block; color:var(--muted); font-size:11px; margin:2px 0; overflow-wrap:anywhere; }
45
- #notes { border-top:1px solid var(--rule); margin-top:6px; padding-top:5px; color:var(--muted); font-size:10px; }
46
- #notes p { margin:2px 0; overflow-wrap:anywhere; white-space:pre-wrap; }
47
- #fallback { font:inherit; font-size:11px; margin:6px 0 0; white-space:pre-wrap; overflow-wrap:anywhere; unicode-bidi:plaintext; }
15
+ html,body { margin:0; background:transparent; min-width:0; }
16
+ .label,.detail,.badge { overflow-wrap:anywhere; min-width:0; unicode-bidi:plaintext; }
17
+ .mark-s { font:600 12px var(--mono); width:1ch; }
18
+ #trace { background:var(--stone); color:var(--text); border:1px solid var(--rule); border-radius:6px; padding:9px 12px; font:13px/1.45 var(--sans); min-width:0; }
19
+ .head { display:flex; flex-wrap:wrap; align-items:baseline; gap:2px 10px; }
20
+ .mark { color:var(--gold); font-size:14px; line-height:1; }
21
+ .headline { flex:1 1 16em; margin:0; min-width:0; overflow-wrap:anywhere; }
22
+ #kind { color:var(--muted); }
23
+ #target { color:var(--papyrus); font:600 13px/1.45 var(--mono); }
24
+ #target,.legend > span,.rows .label { unicode-bidi:isolate; }
25
+ #title { color:var(--papyrus); font-weight:600; }
26
+ #summary { margin-left:8px; }
27
+ .num { color:var(--gold); font:600 12.5px var(--mono); }
28
+ .meta { margin-left:auto; display:flex; gap:10px; white-space:nowrap; font-size:12px; }
29
+ #verdict[data-state="error"] { color:var(--red); } #verdict[data-state="warning"] { color:var(--amber); } #verdict[data-state="ok"] { color:var(--green); }
30
+ #sha { color:var(--muted); font:11.5px var(--mono); }
31
+ .bar { position:relative; display:flex; gap:2px; height:6px; margin:8px 0 0; border-radius:3px; overflow:hidden; background:var(--track); }
32
+ .bar i,.mini i { display:block; flex:0 1 0; min-width:0; }
33
+ .bar i[data-shown] { min-width:3px; }
34
+ .s0 { background:var(--s0); } .s1 { background:var(--s1); } .s2 { background:var(--s2); } .s3 { background:var(--s3); } .s4 { background:var(--s4); }
35
+ .muted { background:var(--s-muted); } .warning { background:var(--s-warning); } .error { background:var(--s-error); }
36
+ .foot { display:flex; align-items:baseline; gap:4px 14px; margin-top:6px; flex-wrap:wrap; }
37
+ .legend { display:flex; flex-wrap:wrap; gap:2px 14px; margin:0; color:var(--muted); font-size:12px; min-width:0; flex:1 1 auto; }
38
+ #trace[data-reveal] .legend { opacity:0; }
39
+ .legend b { color:var(--text); font:500 11.5px var(--mono); margin-left:4px; }
40
+ .sw { display:inline-block; width:8px; height:8px; border-radius:1px; margin-right:5px; }
41
+ .toggle { margin-left:auto; display:flex; align-items:baseline; gap:4px; border:0; background:none; padding:2px 0; color:var(--gold); font:12px var(--sans); cursor:pointer; white-space:nowrap; }
42
+ .toggle:hover { color:var(--papyrus); }
43
+ .toggle:focus-visible,summary:focus-visible { outline:2px solid var(--gold); outline-offset:2px; border-radius:2px; }
44
+ .chev { display:inline-block; font-size:10px; }
45
+ .toggle[aria-expanded="true"] .chev { transform:rotate(90deg); }
46
+ .caveat { margin:6px 0 0; color:var(--amber); font-size:12px; overflow-wrap:anywhere; }
47
+ .caveat span + span::before { content:" · "; color:var(--muted); }
48
+ .rows { list-style:none; margin:4px 0 0; padding:0; }
49
+ .rows li { display:flex; flex-wrap:wrap; align-items:baseline; gap:0 8px; padding:4px 0 5px; border-top:1px solid var(--rule); min-width:0; }
50
+ .rows li:first-child { border-top:0; }
51
+ .rows .kind { color:var(--muted); font-size:12.5px; }
52
+ .rows .label { color:var(--papyrus); font:12.5px var(--mono); }
53
+ .rows .prose { color:var(--text); font:12.5px var(--sans); }
54
+ .rows .detail { color:var(--text); font-size:12.5px; }
55
+ .rows .end { margin-left:auto; font-size:12px; white-space:nowrap; }
56
+ .rows .more { color:var(--muted); font-size:12px; }
57
+ .rows .mini { display:flex; gap:1px; flex:1 0 100%; height:3px; margin-top:3px; border-radius:2px; overflow:hidden; background:var(--track); }
58
+ [data-state="ok"] > .mark-s { color:var(--green); } [data-state="warning"] > .mark-s, [data-state="pending"] > .mark-s { color:var(--amber); } [data-state="error"] > .mark-s, .end[data-state="error"] { color:var(--red); } .end[data-state="warning"] { color:var(--amber); }
59
+ .drawer { display:grid; grid-template-rows:0fr; }
60
+ .drawer[data-open] { grid-template-rows:1fr; }
61
+ .drawer-inner { overflow:hidden; min-height:0; }
62
+ .drawer[data-open] .drawer-inner { border-top:1px solid var(--rule); margin-top:8px; padding-top:4px; }
63
+ ul.tree,.tree ul { list-style:none; margin:0; padding:0; }
64
+ .tree ul { margin:0 0 4px 6px; padding-left:10px; border-left:1px solid var(--rule); }
65
+ .tree summary,.leaf { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:baseline; padding:3px 0; font-size:12.5px; min-width:0; }
66
+ .tree summary { list-style:none; cursor:pointer; color:var(--text); }
67
+ .tree summary::-webkit-details-marker { display:none; }
68
+ .tree summary:hover { background:var(--hover); }
69
+ .tree summary .label::before { content:"▸"; display:inline-block; color:var(--gold); font-size:10px; width:12px; }
70
+ .tree details[open] > summary .label::before { transform:rotate(90deg); }
71
+ .tree details[open] > summary .label { color:var(--gold); }
72
+ .leaf .label { color:var(--papyrus); font:12px var(--mono); }
73
+ .tree > li > .leaf { padding-left:12px; }
74
+ .tree > li > .leaf .label { color:var(--text); font:12.5px var(--sans); }
75
+ .tree .label .detail { color:var(--muted); font:11px var(--mono); margin-left:8px; }
76
+ .tree .label .caveat { color:var(--amber); font:11.5px var(--sans); margin:0 0 0 8px; }
77
+ .badge { color:var(--muted); font:11.5px var(--mono); text-align:right; white-space:nowrap; }
78
+ .badge[data-state="ok"] { color:var(--green); } .badge[data-state="warning"],.badge[data-state="pending"] { color:var(--amber); } .badge[data-state="error"] { color:var(--red); }
79
+ .meter { display:block; height:3px; margin-top:3px; border-radius:2px; background:var(--track); max-width:240px; }
80
+ .meter i { display:block; height:100%; border-radius:2px; background:var(--gold); }
81
+ .notes,.provenance { color:var(--muted); font-size:11.5px; margin:6px 0 2px; }
82
+ .notes p { margin:2px 0; overflow-wrap:anywhere; white-space:pre-wrap; }
83
+ #fallback { font:11.5px/1.5 var(--mono); color:var(--text); margin:6px 0 0; white-space:pre-wrap; overflow-wrap:anywhere; unicode-bidi:plaintext; }
84
+ @media(max-width:400px) { #trace { padding:8px 10px; } #summary { margin-left:0; } #summary:not([hidden]) { display:block; } .tree summary,.leaf { grid-template-columns:minmax(0,1fr); gap:0; } .badge { text-align:left; } }
85
+ /* Motion only for people who have not asked to reduce it. */
86
+ @media(prefers-reduced-motion:no-preference) {
87
+ .bar i,.mini i { transition:flex-grow .7s cubic-bezier(.22,.8,.24,1); }
88
+ .legend { transition:opacity .4s ease .2s; }
89
+ .chev { transition:transform .3s ease; }
90
+ .drawer { transition:grid-template-rows .3s ease; }
91
+ .tree summary .label::before { transition:transform .2s ease; }
92
+ #trace[data-state="pending"] .bar::after,#trace[data-state="waiting"] .bar::after { content:""; position:absolute; top:0; bottom:0; left:0; width:35%; background:linear-gradient(90deg,transparent,var(--gold),transparent); opacity:.55; animation:sweep 1.3s ease-in-out infinite; }
93
+ @keyframes sweep { from { transform:translateX(-100%); } to { transform:translateX(290%); } }
94
+ }
48
95
  [hidden] { display:none; }
49
- @media(max-width:400px) { #trace { padding:8px 10px 7px; } summary,.row { grid-template-columns:15px minmax(0,1fr); gap:4px; } .badge { grid-column:2; text-align:left; } .children { margin-left:9px; padding-left:7px; } }
50
- @media(prefers-reduced-motion:reduce) { #trace[data-pending="true"] #status { animation:none; } }
51
96
  </style>
52
97
  </head>
53
98
  <body>
54
- <main id="trace" data-pending="false">
55
- <header><span id="tool">Pharaoh result</span><span id="status" role="status" aria-live="polite" aria-atomic="true">waiting</span></header>
56
- <div id="repo" hidden></div>
57
- <div id="provenance" hidden></div>
58
- <h1 id="title">Waiting for a tool result</h1>
59
- <p id="summary" hidden></p>
60
- <ul id="tree" aria-label="Result evidence" hidden></ul>
61
- <pre id="fallback">The tool's text response remains available in chat.</pre>
62
- <div id="notes" hidden></div>
99
+ <main id="trace" data-state="waiting">
100
+ <div class="head">
101
+ <span class="mark" aria-hidden="true">☥</span>
102
+ <p class="headline"><span id="kind"></span><span id="target"></span><span id="title">Pharaoh</span><span id="summary" role="status" aria-live="polite">Waiting for the tool</span></p>
103
+ <span id="meta" class="meta"><span id="verdict" hidden></span><span id="sha" hidden></span></span>
104
+ </div>
105
+ <div id="bar" class="bar" aria-hidden="true"></div>
106
+ <div id="foot" class="foot">
107
+ <p id="legend" class="legend"></p>
108
+ <button id="toggle" class="toggle" type="button" aria-expanded="false" aria-controls="drawer" hidden><span id="toggle-label">Details</span><span class="chev" aria-hidden="true">▸</span></button>
109
+ </div>
110
+ <p id="caveat" class="caveat" hidden></p>
111
+ <ul id="rows" class="rows" hidden></ul>
112
+ <div id="drawer" class="drawer" inert>
113
+ <div class="drawer-inner">
114
+ <ul id="tree" class="tree" aria-label="Result evidence"></ul>
115
+ <pre id="fallback" hidden></pre>
116
+ <div id="notes" class="notes" hidden></div>
117
+ <p id="provenance" class="provenance" hidden></p>
118
+ </div>
119
+ </div>
63
120
  </main>
64
121
  <script>
65
122
  (() => {
@@ -67,11 +124,18 @@
67
124
  const META_KEY = "so.pharaoh/trace";
68
125
  const PROTOCOL_VERSION = "2026-01-26";
69
126
  // Kept in parity with TRACE_LIMITS in trace-contract.ts by the resource test.
70
- const LIMITS = { bytes:65536, nodes:120, depth:6, label:512, detail:2048, badge:128, summary:1024, notes:16, note:1024, repo:256 };
127
+ const LIMITS = { bytes:65536, nodes:120, depth:6, label:512, detail:2048, badge:128, summary:1024, notes:16, note:1024, repo:256, segments:8 };
71
128
  const MAX_TEXT = 32768;
129
+ const MAX_ROWS = 5;
130
+ const TONES = ["warning","error","muted"];
131
+ const STATES = ["ok","pending","warning","error"];
132
+ // What each tool is doing while it runs, shown with the argument it targets.
133
+ const VERBS = { get_blast_radius:"Tracing callers of", get_unused_code:"Checking reachability in", search_functions:"Searching for", get_codebase_map:"Mapping", get_module_context:"Reading module", pharaoh_recon:"Running recon on", pharaoh_account:"Checking account" };
72
134
  const root = document.getElementById("trace");
73
- const fields = Object.fromEntries(["tool","status","repo","provenance","title","summary","tree","fallback","notes"].map(id => [id,document.getElementById(id)]));
135
+ const fields = Object.fromEntries(["kind","target","title","summary","meta","verdict","sha","bar","caveat","rows","foot","legend","toggle","toggle-label","drawer","tree","fallback","notes","provenance"].map(id => [id,document.getElementById(id)]));
136
+ const compact = new Intl.NumberFormat("en",{notation:"compact",maximumFractionDigits:1});
74
137
  const initId = "pharaoh-trace-initialize";
138
+ let tool = "";
75
139
  let ready = false;
76
140
  let closed = false;
77
141
  let cancelled = false;
@@ -81,37 +145,76 @@
81
145
  const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
82
146
  const string = (value,max) => typeof value === "string" && value.length <= max;
83
147
  const send = message => { if (!closed) window.parent.postMessage({jsonrpc:"2.0",...message},"*"); };
84
- function status(label,state) {
85
- fields.status.textContent = label;
86
- fields.status.dataset.state = state;
87
- root.dataset.pending = String(state === "pending");
148
+ function element(tag,className,text) {
149
+ const node = document.createElement(tag);
150
+ if (className) node.className = className;
151
+ if (text !== undefined) node.textContent = text;
152
+ return node;
153
+ }
154
+ // Counts read at a glance: digit runs become gold monospace spans, everything else stays text.
155
+ function rich(target,text) {
156
+ target.replaceChildren();
157
+ for (const part of String(text || "").split(/(\b\d[\d,.]*\b)/)) {
158
+ if (part) target.append(/^\d/.test(part) ? element("span","num",part) : part);
159
+ }
88
160
  }
89
161
  function display(field,text) {
90
162
  fields[field].textContent = text || "";
91
163
  fields[field].hidden = !text;
92
164
  }
165
+ // "Impact: attachTraceView" reads as a muted kind and a monospace target.
166
+ const splitTitle = title => /^([^:]{1,40}): (.+)$/.exec(title);
167
+ function headline(title,summary) {
168
+ const split = splitTitle(title);
169
+ display("kind",split ? `${split[1]} ` : "");
170
+ display("target",split ? split[2] : "");
171
+ display("title",split ? "" : title);
172
+ rich(fields.summary,summary);
173
+ fields.summary.hidden = !summary;
174
+ }
175
+ // The toggle ends the legend line, or the headline when there is no legend.
176
+ function place(withLegend) {
177
+ (withLegend ? fields.foot : fields.meta).append(fields.toggle);
178
+ fields.foot.hidden = !withLegend;
179
+ }
180
+ function setOpen(open) {
181
+ fields.drawer.toggleAttribute("data-open",open);
182
+ fields.drawer.toggleAttribute("inert",!open);
183
+ fields.toggle.setAttribute("aria-expanded",String(open));
184
+ fields["toggle-label"].textContent = open ? "Hide" : "Details";
185
+ }
186
+ function clearResult() {
187
+ setOpen(false);
188
+ for (const field of ["bar","rows","legend","tree","notes"]) fields[field].replaceChildren();
189
+ for (const field of ["verdict","sha","caveat","provenance","fallback"]) display(field,"");
190
+ delete fields.verdict.dataset.state;
191
+ fields.sha.title = "";
192
+ fields.bar.hidden = true;
193
+ fields.rows.hidden = true;
194
+ fields.notes.hidden = true;
195
+ fields.toggle.hidden = true;
196
+ }
93
197
  function notes(items) {
94
- fields.notes.replaceChildren();
95
- for (const text of items) {
96
- const paragraph = document.createElement("p");
97
- paragraph.textContent = text;
98
- fields.notes.append(paragraph);
99
- }
198
+ for (const text of items) fields.notes.append(element("p","",text));
100
199
  fields.notes.hidden = items.length === 0;
101
200
  }
102
- function clearResult() {
103
- fields.tree.replaceChildren();
104
- fields.tree.hidden = true;
105
- display("summary","");
106
- display("provenance","");
107
- notes([]);
201
+ function caveats(items) {
202
+ fields.caveat.replaceChildren(...items.map(text => element("span","",text)));
203
+ fields.caveat.hidden = items.length === 0;
108
204
  }
109
205
  function fallback(title,text,notice,state="neutral") {
110
206
  clearResult();
111
- fields.title.textContent = title;
207
+ root.dataset.state = state === "error" ? "error" : "ready";
208
+ const first = text.split("\n").find(line => line.trim()) || "";
209
+ headline(title,first.length > 160 ? `${first.slice(0,159)}…` : first);
112
210
  display("fallback",text);
113
- status(state === "error" ? "[!] error" : state === "cancelled" ? "[||] cancelled" : "response received",state);
211
+ if (state === "error" || state === "cancelled") {
212
+ fields.verdict.dataset.state = state === "error" ? "error" : "warning";
213
+ display("verdict",state === "error" ? "Failed" : "Cancelled");
214
+ }
114
215
  if (notice) notes([notice]);
216
+ fields.toggle.hidden = !text;
217
+ place(false);
115
218
  }
116
219
  function readableText(result) {
117
220
  if (!record(result) || !Array.isArray(result.content)) return {text:"No readable text was returned. Read the tool response in chat.",omitted:false};
@@ -129,12 +232,20 @@
129
232
  }
130
233
  return {text:text || "No readable text was returned. Read the tool response in chat.",omitted};
131
234
  }
235
+ function validGlance(glance) {
236
+ if (glance === undefined) return true;
237
+ if (!record(glance)) return false;
238
+ if (glance.segments !== undefined && (!Array.isArray(glance.segments) || glance.segments.length > LIMITS.segments || !glance.segments.every(s => record(s) && string(s.label,LIMITS.badge) && Number.isFinite(s.value) && s.value >= 0 && (s.tone === undefined || TONES.includes(s.tone))))) return false;
239
+ if (glance.verdict !== undefined && (!record(glance.verdict) || !string(glance.verdict.label,LIMITS.badge) || (glance.verdict.status !== undefined && !["ok","warning","error"].includes(glance.verdict.status)))) return false;
240
+ return glance.caveat === undefined || string(glance.caveat,LIMITS.note);
241
+ }
132
242
  function validView(view) {
133
243
  try {
134
244
  if (!record(view) || view.version !== 1 || !string(view.title,LIMITS.label) || !view.title.trim() || !Array.isArray(view.nodes)) return false;
135
245
  if (new TextEncoder().encode(JSON.stringify(view)).byteLength > LIMITS.bytes) return false;
136
246
  if (view.repo !== undefined && !string(view.repo,LIMITS.repo)) return false;
137
247
  if (view.summary !== undefined && !string(view.summary,LIMITS.summary)) return false;
248
+ if (!validGlance(view.glance)) return false;
138
249
  if (view.notes !== undefined && (!Array.isArray(view.notes) || view.notes.length > LIMITS.notes || !view.notes.every(note => string(note,LIMITS.note)))) return false;
139
250
  if (view.freshness !== undefined) {
140
251
  const value = view.freshness;
@@ -145,71 +256,145 @@
145
256
  if (++count > LIMITS.nodes || depth > LIMITS.depth || !record(node) || !string(node.label,LIMITS.label) || !node.label.trim()) return false;
146
257
  if (node.detail !== undefined && !string(node.detail,LIMITS.detail)) return false;
147
258
  if (node.badge !== undefined && !string(node.badge,LIMITS.badge)) return false;
148
- if (node.status !== undefined && !["ok","pending","warning","error"].includes(node.status)) return false;
259
+ if (node.status !== undefined && !STATES.includes(node.status)) return false;
149
260
  if (node.meter !== undefined && (!record(node.meter) || !Number.isFinite(node.meter.value) || !Number.isFinite(node.meter.total) || node.meter.total <= 0 || node.meter.value < 0 || node.meter.value > node.meter.total)) return false;
261
+ if (!validGlance(node.glance)) return false;
150
262
  return node.children === undefined || (Array.isArray(node.children) && node.children.every(child => visit(child,depth + 1)));
151
263
  };
152
264
  return view.nodes.every(node => visit(node,1));
153
265
  } catch { return false; }
154
266
  }
155
- function element(tag,className,text) {
156
- const node = document.createElement(tag);
157
- if (className) node.className = className;
158
- if (text !== undefined) node.textContent = text;
159
- return node;
267
+ // Segments start empty and grow to their share once painted: the one motion on arrival.
268
+ function segments(target,items) {
269
+ const total = items.reduce((sum,item) => sum + item.value,0);
270
+ if (!total) return [];
271
+ let shade = 0;
272
+ return items.filter(item => item.value > 0).map((item,index) => {
273
+ const slice = element("i",item.tone || `s${Math.min(shade++,4)}`);
274
+ slice.style.transitionDelay = `${index * 70}ms`;
275
+ slice.dataset.share = String(item.value / total);
276
+ target.append(slice);
277
+ return slice;
278
+ });
279
+ }
280
+ function grow(slices) {
281
+ requestAnimationFrame(() => requestAnimationFrame(() => {
282
+ for (const slice of slices) { slice.style.flexGrow = slice.dataset.share; slice.dataset.shown = ""; }
283
+ delete root.dataset.reveal;
284
+ }));
285
+ }
286
+ function legend(items) {
287
+ let shade = 0;
288
+ for (const item of items.filter(item => item.value > 0)) {
289
+ const entry = element("span");
290
+ entry.append(element("span",`sw ${item.tone || `s${Math.min(shade++,4)}`}`),item.label,element("b","",compact.format(item.value)));
291
+ fields.legend.append(entry);
292
+ }
160
293
  }
161
- function renderNode(node,last,open=false) {
294
+ function verdict(target,value) {
295
+ if (!value) return;
296
+ target.textContent = value.label;
297
+ if (value.status) target.dataset.state = value.status;
298
+ target.hidden = false;
299
+ }
300
+ // Sections (recon) and status lines (states, named checks) stay visible; groups open on demand.
301
+ function rowFor(node,slices) {
302
+ const item = element("li");
303
+ if (node.status) {
304
+ item.dataset.state = node.status;
305
+ item.append(element("span","mark-s",{ok:"✓",pending:"…",warning:"!",error:"×"}[node.status]));
306
+ }
307
+ const split = splitTitle(node.label);
308
+ if (split) item.append(element("span","kind",split[1]));
309
+ // Names read in monospace; sentences (state messages) read as prose.
310
+ item.append(element("span",split || !/\s/.test(node.label) ? "label" : "label prose",split ? split[2] : node.label));
311
+ if (node.detail) { const detail = element("span","detail"); rich(detail,node.detail); item.append(detail); }
312
+ const end = element("span","end",node.badge || "");
313
+ end.hidden = !node.badge;
314
+ if (node.glance?.verdict) { end.hidden = true; verdict(end,node.glance.verdict); }
315
+ item.append(end);
316
+ if (node.glance?.caveat) item.append(element("span","detail caveat",node.glance.caveat));
317
+ if (node.glance?.segments?.length) {
318
+ const mini = element("span","mini");
319
+ mini.setAttribute("aria-hidden","true");
320
+ slices.push(...segments(mini,node.glance.segments));
321
+ item.append(mini);
322
+ }
323
+ return item;
324
+ }
325
+ function renderNode(node) {
162
326
  const item = element("li");
163
327
  const group = Array.isArray(node.children) && node.children.length > 0;
164
- const container = group ? element("details") : item;
165
- if (group && open) container.open = true;
166
- const row = element(group ? "summary" : "div",group ? "" : "row");
167
- const branch = element("span","branch",group ? "" : last ? "└─" : "├─");
168
- branch.setAttribute("aria-hidden","true");
328
+ const row = element(group ? "summary" : "div",group ? "" : "leaf");
169
329
  const label = element("span","label",node.label);
170
330
  if (node.detail) label.append(element("span","detail",node.detail));
171
- const symbol = {ok:"[ok]",pending:"[...]",warning:"[!]",error:"[!]"}[node.status] || "";
172
- const badge = element("span","badge",[symbol,node.badge].filter(Boolean).join(" "));
173
- if (node.status) badge.dataset.state = node.status;
174
- row.append(branch,label,badge);
175
- container.append(row);
176
331
  if (node.meter) {
177
- const filled = Math.round(node.meter.value / node.meter.total * 12);
178
332
  const meter = element("span","meter");
179
- meter.append("[",element("span","filled","#".repeat(filled)),`${".".repeat(12-filled)}] ${node.meter.value} / ${node.meter.total}`);
333
+ meter.title = `${node.meter.value} / ${node.meter.total}`;
334
+ const fill = element("i");
335
+ fill.style.width = `${Math.max(1,Math.round(node.meter.value / node.meter.total * 100))}%`;
336
+ meter.append(fill);
180
337
  label.append(meter);
181
338
  }
182
- if (group) {
183
- const children = element("ul","children");
184
- node.children.forEach((child,index) => { children.append(renderNode(child,index === node.children.length-1)); });
185
- container.append(children);
186
- item.append(container);
187
- }
339
+ if (node.glance?.caveat) label.append(element("span","caveat",node.glance.caveat));
340
+ const symbol = {ok:"✓",pending:"…",warning:"!",error:"×"}[node.status] || "";
341
+ const badge = element("span","badge",[symbol,node.badge || node.glance?.verdict?.label].filter(Boolean).join(" "));
342
+ const state = node.status || node.glance?.verdict?.status;
343
+ if (state) badge.dataset.state = state;
344
+ row.append(label,badge);
345
+ if (!group) { item.append(row); return item; }
346
+ const details = element("details");
347
+ const children = element("ul");
348
+ for (const child of node.children) children.append(renderNode(child));
349
+ details.append(row,children);
350
+ item.append(details);
188
351
  return item;
189
352
  }
190
353
  function renderView(view) {
191
354
  clearResult();
192
- fields.title.textContent = view.title;
193
- display("repo",view.repo);
194
- display("summary",view.summary);
195
- display("fallback","");
196
- if (view.freshness) {
197
- const freshness = view.freshness;
198
- display("provenance",[freshness.mappedCommit ? `mapped ${freshness.mappedCommit}` : "mapped commit unknown",freshness.lastAnalyzed ? `as of ${freshness.lastAnalyzed}` : "map time unknown",`status: ${freshness.status}`].join(" · "));
355
+ cancelled = false;
356
+ root.dataset.state = "ready";
357
+ root.dataset.reveal = "";
358
+ const glance = view.glance || {};
359
+ headline(view.title,view.summary);
360
+ verdict(fields.verdict,glance.verdict);
361
+ const freshness = view.freshness;
362
+ if (freshness?.mappedCommit) {
363
+ display("sha",`@${/^[0-9a-f]{7,}$/i.test(freshness.mappedCommit) ? freshness.mappedCommit.slice(0,7) : freshness.mappedCommit.slice(0,12)}`);
364
+ fields.sha.title = `Mapped ${freshness.mappedCommit}${freshness.lastAnalyzed ? ` as of ${freshness.lastAnalyzed}` : ""}`;
199
365
  }
200
- const firstGroup = view.nodes.findIndex(node => node.children?.length);
201
- view.nodes.forEach((node,index) => { fields.tree.append(renderNode(node,index === view.nodes.length-1,index === firstGroup)); });
202
- fields.tree.hidden = view.nodes.length === 0;
366
+ if (freshness) display("provenance",[freshness.mappedCommit ? `Mapped ${freshness.mappedCommit}` : "Mapped commit unknown",freshness.lastAnalyzed ? `as of ${freshness.lastAnalyzed}` : "map time unknown",`status: ${freshness.status}`].join(" · "));
367
+ caveats([glance.caveat,freshness?.status === "stale" ? "Map is behind the latest push" : ""].filter(Boolean));
368
+ const slices = segments(fields.bar,glance.segments || []);
369
+ fields.bar.hidden = slices.length === 0;
370
+ legend(glance.segments || []);
371
+ // Up to MAX_ROWS rows, warnings and verdicts first, shown in their original order.
372
+ const candidates = view.nodes.filter(node => node.glance || node.status);
373
+ const urgent = node => node.status === "error" || node.status === "warning" || node.glance?.verdict || node.glance?.caveat;
374
+ const chosen = new Set([...new Set([...candidates.filter(urgent),...candidates])].slice(0,MAX_ROWS));
375
+ const visible = candidates.filter(node => chosen.has(node));
376
+ for (const node of visible) fields.rows.append(rowFor(node,slices));
377
+ if (candidates.length > visible.length) fields.rows.append(element("li","more",`+${candidates.length - visible.length} more in Details`));
378
+ fields.rows.hidden = visible.length === 0;
379
+ for (const node of view.nodes) if (!visible.includes(node) || node.children?.length) fields.tree.append(renderNode(node));
380
+ // One click on Details shows evidence: the first group is already open inside the drawer.
381
+ fields.tree.querySelector("details")?.setAttribute("open","");
203
382
  notes(view.notes || []);
204
- status("[ok] result","ok");
383
+ fields.toggle.hidden = !fields.tree.childElementCount && fields.notes.hidden && fields.provenance.hidden;
384
+ place(fields.legend.childElementCount > 0);
385
+ grow(slices);
205
386
  }
206
387
  function input(params) {
207
388
  cancelled = false;
208
389
  clearResult();
209
- display("fallback","Waiting for the tool response.");
210
- fields.title.textContent = "Request in progress";
211
- display("repo",record(params.arguments) && string(params.arguments.repo,LIMITS.repo) ? params.arguments.repo : "");
212
- status("[>] working","pending");
390
+ root.dataset.state = "pending";
391
+ const args = record(params.arguments) ? params.arguments : {};
392
+ const target = ["entity","query","module","repo"].map(key => args[key]).find(value => string(value,LIMITS.repo) && value.trim());
393
+ const verb = Object.hasOwn(VERBS,tool) ? VERBS[tool] : "Querying the Pharaoh graph";
394
+ headline("","");
395
+ display("kind",target ? `${verb} ` : `${verb}…`);
396
+ display("target",target ? `${target}…` : "");
397
+ fields.bar.hidden = false;
213
398
  }
214
399
  function result(params) {
215
400
  if (cancelled) return;
@@ -226,7 +411,7 @@
226
411
  function context(params) {
227
412
  if (params.theme === "dark" || params.theme === "light") document.documentElement.dataset.theme = params.theme;
228
413
  const name = params.toolInfo?.tool?.name;
229
- if (string(name,128)) fields.tool.textContent = name;
414
+ if (string(name,128)) tool = name;
230
415
  }
231
416
  function size() {
232
417
  if (!ready || closed || resizeFrame) return;
@@ -244,7 +429,9 @@
244
429
  const observer = typeof ResizeObserver === "function" ? new ResizeObserver(size) : null;
245
430
  observer?.observe(root);
246
431
  root.addEventListener("toggle",size,true);
432
+ root.addEventListener("transitionend",size);
247
433
  window.addEventListener("resize",size);
434
+ fields.toggle.addEventListener("click",() => { setOpen(fields.toggle.getAttribute("aria-expanded") !== "true"); size(); });
248
435
  function receive(event) {
249
436
  if (closed || event.source !== window.parent || !record(event.data) || event.data.jsonrpc !== "2.0") return;
250
437
  const message = event.data;
@@ -271,7 +458,7 @@
271
458
  clearTimeout(initTimeout);
272
459
  cancelAnimationFrame(resizeFrame);
273
460
  observer?.disconnect();
274
- root.dataset.pending = "false";
461
+ root.dataset.state = "ready";
275
462
  window.removeEventListener("message",receive);
276
463
  } else send({id:message.id,error:{code:-32601,message:"Unsupported request"}});
277
464
  return;
@@ -289,7 +476,7 @@
289
476
  const initTimeout = setTimeout(() => {
290
477
  if (!ready && !closed) fallback("Text response available","Read the tool response in chat.","Trace has not received a host connection.");
291
478
  },10000);
292
- send({id:initId,method:"ui/initialize",params:{appInfo:{name:"Pharaoh Trace",version:"1.1.0"},appCapabilities:{availableDisplayModes:["inline"]},protocolVersion:PROTOCOL_VERSION}});
479
+ send({id:initId,method:"ui/initialize",params:{appInfo:{name:"Pharaoh Trace",version:"1.2.0"},appCapabilities:{availableDisplayModes:["inline"]},protocolVersion:PROTOCOL_VERSION}});
293
480
  })();
294
481
  </script>
295
482
  </body>