@gravitylabsllc/porthole 0.1.0

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/dist/report.js ADDED
@@ -0,0 +1,161 @@
1
+ import { frameBudgetMs } from "./trace.js";
2
+ /** Lanes a reader should be told were checked and found quiet. */
3
+ const CHECKED = [
4
+ { label: "http", keys: ["http.failed"] },
5
+ { label: "db", keys: ["db.onMainThread"] },
6
+ { label: "main thread", keys: ["mainThread.stalls"] },
7
+ { label: "frames", keys: ["frames.missed"] },
8
+ { label: "work", keys: ["work.retries", "work.failures"] },
9
+ { label: "memory", keys: ["memory.blockingGcMs"] },
10
+ ];
11
+ const LABEL = {
12
+ error: "ERROR ",
13
+ warning: "WARNING",
14
+ note: "NOTE ",
15
+ };
16
+ export function renderReport(trace) {
17
+ const lines = [];
18
+ const device = trace.device;
19
+ const hz = Number(device.refreshHz) || 60;
20
+ lines.push([
21
+ trace.scenario,
22
+ `${(trace.durationMs / 1000).toFixed(1)}s`,
23
+ `${device.model ?? "unknown device"} (${Math.round(hz)}Hz)`,
24
+ trace.app.packageName,
25
+ ]
26
+ .filter(Boolean)
27
+ .join(" · "));
28
+ lines.push("");
29
+ if (trace.findings.length === 0) {
30
+ lines.push(" nothing worth reporting");
31
+ }
32
+ // Severity order, not grouped by mark. Grouping reads well until the first
33
+ // marked run, where it drops an ERROR below two WARNINGs and defeats the one
34
+ // job of a prioritised list. The mark rides along on the line instead.
35
+ for (const finding of trace.findings) {
36
+ lines.push(` ${LABEL[finding.severity]} ${finding.title}`);
37
+ if (finding.during)
38
+ lines.push(` during "${finding.during}"`);
39
+ if (finding.detail)
40
+ lines.push(` ${finding.detail}`);
41
+ }
42
+ const quiet = CHECKED.filter((lane) => lane.keys.every((key) => (trace.metrics[key] ?? 0) === 0)).map((lane) => lane.label);
43
+ // Saying what was checked and found clean matters as much as the findings. A
44
+ // report that only ever lists problems gives no signal that the things it did
45
+ // not mention were looked at.
46
+ if (quiet.length > 0) {
47
+ lines.push("");
48
+ lines.push(` quiet: ${quiet.join(", ")}`);
49
+ }
50
+ lines.push("");
51
+ lines.push(` frame budget ${frameBudgetMs(hz)}ms · ` +
52
+ `${trace.metrics["recompose.total"]} recompositions · ` +
53
+ `${trace.metrics["http.calls"]} calls · ${trace.metrics["db.queries"]} queries`);
54
+ if (trace.marks.length > 0)
55
+ lines.push(` ${trace.marks.length} marks`);
56
+ if (trace.driver)
57
+ lines.push(` driver: ${trace.driver}`);
58
+ return lines.join("\n") + "\n";
59
+ }
60
+ /** Metrics where a larger number is better. Everything else is the other way. */
61
+ const HIGHER_IS_BETTER = new Set();
62
+ /**
63
+ * Below both of these, a difference is noise.
64
+ *
65
+ * Timing metrics move run to run. With only a relative floor a small absolute
66
+ * change looks enormous; with only an absolute one a large metric never moves
67
+ * enough. Without both, every run is a regression and the check gets switched
68
+ * off, which is the real failure mode.
69
+ */
70
+ const RELATIVE_FLOOR = 0.1;
71
+ const ABSOLUTE_FLOOR = 3;
72
+ export function compareMetrics(before, after) {
73
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
74
+ return keys.map((key) => {
75
+ const a = before[key] ?? 0;
76
+ const b = after[key] ?? 0;
77
+ const worse = HIGHER_IS_BETTER.has(key) ? b < a : b > a;
78
+ // Categorical, not a drift: the first main-thread query, the first failing
79
+ // call. No floor should hide one of these.
80
+ if (a === 0 && b > 0)
81
+ return { key, before: a, after: b, kind: "new" };
82
+ if (a === b)
83
+ return { key, before: a, after: b, kind: "unchanged" };
84
+ const absolute = Math.abs(b - a);
85
+ const relative = a === 0 ? 1 : absolute / a;
86
+ if (absolute < ABSOLUTE_FLOOR || relative < RELATIVE_FLOOR) {
87
+ return { key, before: a, after: b, kind: "unchanged" };
88
+ }
89
+ return { key, before: a, after: b, kind: worse ? "regressed" : "improved" };
90
+ });
91
+ }
92
+ /** Why two traces cannot honestly be compared, or null if they can. */
93
+ export function comparability(before, after) {
94
+ if (before.scenario !== after.scenario) {
95
+ return `different scenarios: "${before.scenario}" and "${after.scenario}"`;
96
+ }
97
+ const a = before.device;
98
+ const b = after.device;
99
+ // Only a known difference counts. A capture that attached to an app already
100
+ // running may not have seen the device profile, and two absent values are not
101
+ // a mismatch — compared as numbers they become NaN !== NaN, which refuses
102
+ // every pair of traces that happen to be missing the same field.
103
+ const differs = (key) => {
104
+ const left = a[key];
105
+ const right = b[key];
106
+ if (left === undefined || right === undefined)
107
+ return false;
108
+ return left !== right;
109
+ };
110
+ if (differs("refreshHz")) {
111
+ return `different refresh rates: ${a.refreshHz}Hz and ${b.refreshHz}Hz — frame budgets differ`;
112
+ }
113
+ if (differs("cores")) {
114
+ return `different core counts: ${a.cores} and ${b.cores}`;
115
+ }
116
+ if (differs("lowRamDevice")) {
117
+ return "one capture is from a low-RAM device and the other is not";
118
+ }
119
+ return null;
120
+ }
121
+ export function renderComparison(before, after) {
122
+ const blocked = comparability(before, after);
123
+ if (blocked) {
124
+ return {
125
+ text: `refusing to compare: ${blocked}\n\n` +
126
+ " A number from two runs that were never comparable is worse than no\n" +
127
+ " number, because someone will act on it.\n",
128
+ regressed: false,
129
+ refused: true,
130
+ };
131
+ }
132
+ const changes = compareMetrics(before.metrics, after.metrics);
133
+ const notable = changes.filter((c) => c.kind !== "unchanged");
134
+ const unchanged = changes.length - notable.length;
135
+ const lines = [];
136
+ lines.push(`${after.scenario} · against a baseline of ${before.capturedAt}`);
137
+ lines.push("");
138
+ for (const change of notable) {
139
+ const label = change.kind === "improved" ? "improved " : change.kind.toUpperCase().padEnd(9);
140
+ const delta = change.kind === "new"
141
+ ? "new"
142
+ : `(${change.after > change.before ? "+" : ""}${Math.round(((change.after - change.before) / (change.before || 1)) * 100)}%)`;
143
+ lines.push(` ${label} ${change.key.padEnd(24)} ${change.before} → ${change.after} ${delta}`);
144
+ }
145
+ if (notable.length === 0)
146
+ lines.push(" nothing moved");
147
+ lines.push("");
148
+ lines.push(` unchanged: ${unchanged} other metrics`);
149
+ // An agentic driver reasons between steps and does not walk the same path
150
+ // twice, so its timings drift for reasons that are not the code's.
151
+ if (after.driver && after.driver !== before.driver) {
152
+ lines.push("");
153
+ lines.push(` note: drivers differ ("${before.driver ?? "?"}" then "${after.driver}")`);
154
+ }
155
+ return {
156
+ text: lines.join("\n") + "\n",
157
+ regressed: notable.some((c) => c.kind === "regressed" || c.kind === "new"),
158
+ refused: false,
159
+ };
160
+ }
161
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,kEAAkE;AAClE,MAAM,OAAO,GAA6C;IACxD,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,aAAa,CAAC,EAAE;IACxC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,iBAAiB,CAAC,EAAE;IAC1C,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,mBAAmB,CAAC,EAAE;IACrD,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,eAAe,CAAC,EAAE;IAC5C,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,cAAc,EAAE,eAAe,CAAC,EAAE;IAC1D,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,qBAAqB,CAAC,EAAE;CACnD,CAAC;AAEF,MAAM,KAAK,GAAwC;IACjD,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,SAAS;CAChB,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,KAAY;IACvC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAiC,CAAC;IACvD,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IAE1C,KAAK,CAAC,IAAI,CACR;QACE,KAAK,CAAC,QAAQ;QACd,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;QAC1C,GAAG,MAAM,CAAC,KAAK,IAAI,gBAAgB,KAAK,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK;QAC3D,KAAK,CAAC,GAAG,CAAC,WAAW;KACtB;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,KAAK,CAAC,CACf,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAC1C,CAAC;IAED,2EAA2E;IAC3E,6EAA6E;IAC7E,uEAAuE;IACvE,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QAC7D,IAAI,OAAO,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,sBAAsB,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QACxE,IAAI,OAAO,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,cAAc,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CACpC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAC1D,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAE5B,6EAA6E;IAC7E,8EAA8E;IAC9E,8BAA8B;IAC9B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,kBAAkB,aAAa,CAAC,EAAE,CAAC,OAAO;QACxC,GAAG,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,oBAAoB;QACvD,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,UAAU,CAClF,CAAC;IACF,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC;IACxE,IAAI,KAAK,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAE1D,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC;AASD,iFAAiF;AACjF,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,MAAM,UAAU,cAAc,CAC5B,MAA8B,EAC9B,KAA6B;IAE7B,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAElF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACtB,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3B,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAExD,2EAA2E;QAC3E,2CAA2C;QAC3C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,KAAc,EAAE,CAAC;QAChF,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,WAAoB,EAAE,CAAC;QAE7E,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;QAC5C,IAAI,QAAQ,GAAG,cAAc,IAAI,QAAQ,GAAG,cAAc,EAAE,CAAC;YAC3D,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,WAAoB,EAAE,CAAC;QAClE,CAAC;QAED,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,MAAa,EAAE,KAAY;IACvD,IAAI,MAAM,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;QACvC,OAAO,yBAAyB,MAAM,CAAC,QAAQ,UAAU,KAAK,CAAC,QAAQ,GAAG,CAAC;IAC7E,CAAC;IAED,MAAM,CAAC,GAAG,MAAM,CAAC,MAAiC,CAAC;IACnD,MAAM,CAAC,GAAG,KAAK,CAAC,MAAiC,CAAC;IAElD,4EAA4E;IAC5E,8EAA8E;IAC9E,0EAA0E;IAC1E,iEAAiE;IACjE,MAAM,OAAO,GAAG,CAAC,GAAW,EAAW,EAAE;QACvC,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACpB,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACrB,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAC5D,OAAO,IAAI,KAAK,KAAK,CAAC;IACxB,CAAC,CAAC;IAEF,IAAI,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACzB,OAAO,4BAA4B,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,SAAS,2BAA2B,CAAC;IACjG,CAAC;IACD,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACrB,OAAO,0BAA0B,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;IAC5D,CAAC;IACD,IAAI,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;QAC5B,OAAO,2DAA2D,CAAC;IACrE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAa,EACb,KAAY;IAEZ,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC7C,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO;YACL,IAAI,EACF,wBAAwB,OAAO,MAAM;gBACrC,wEAAwE;gBACxE,6CAA6C;YAC/C,SAAS,EAAE,KAAK;YAChB,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAClD,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,4BAA4B,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IAC7E,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC7F,MAAM,KAAK,GACT,MAAM,CAAC,IAAI,KAAK,KAAK;YACnB,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;QAClI,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,MAAM,MAAM,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC,CAAC;IACjG,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACxD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,gBAAgB,SAAS,gBAAgB,CAAC,CAAC;IAEtD,0EAA0E;IAC1E,mEAAmE;IACnE,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;QACnD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,4BAA4B,MAAM,CAAC,MAAM,IAAI,GAAG,WAAW,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC1F,CAAC;IAED,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI;QAC7B,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC;QAC1E,OAAO,EAAE,KAAK;KACf,CAAC;AACJ,CAAC"}
@@ -0,0 +1,232 @@
1
+ // Copyright 2026 Gravity Labs
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { restartApp } from "./adb.js";
4
+ import http from "node:http";
5
+ import { readFile } from "node:fs/promises";
6
+ import { fileURLToPath } from "node:url";
7
+ import { extname, resolve } from "node:path";
8
+ import { WebSocketServer, WebSocket } from "ws";
9
+ const UI_DIR = fileURLToPath(new URL("../ui/dist/", import.meta.url));
10
+ const CONTENT_TYPES = {
11
+ ".html": "text/html; charset=utf-8",
12
+ ".js": "text/javascript; charset=utf-8",
13
+ ".css": "text/css; charset=utf-8",
14
+ ".svg": "image/svg+xml",
15
+ ".woff2": "font/woff2",
16
+ ".json": "application/json",
17
+ ".map": "application/json",
18
+ };
19
+ /** Roughly ten minutes of a busy app. The device ring is smaller; this is the wider view. */
20
+ const BUFFER_LIMIT = 20_000;
21
+ /**
22
+ * Serves the timeline UI and streams events to it.
23
+ *
24
+ * Kept separate from the MCP transport on purpose: MCP talks stdio to the agent,
25
+ * this talks HTTP and WebSocket to a browser, and the two never cross. The event
26
+ * buffer is shared so the UI and the `timeline` tool see the same history.
27
+ */
28
+ export class TimelineServer {
29
+ device;
30
+ port;
31
+ serial;
32
+ server = null;
33
+ wss = null;
34
+ events = [];
35
+ started = false;
36
+ constructor(device, port, serial) {
37
+ this.device = device;
38
+ this.port = port;
39
+ this.serial = serial;
40
+ device.on("event", (event) => this.record(event));
41
+ device.on("state", (state) => this.broadcast({ type: "state", state }));
42
+ device.on("hello", (hello) => {
43
+ // A process is a session. Sequence numbers restart with it, so keeping the
44
+ // previous process's events would put two timelines on one axis — and,
45
+ // worse, make the new process's first events look like ones already seen.
46
+ const startedAt = hello?.startedAt;
47
+ if (startedAt !== undefined && startedAt !== this.startedAt) {
48
+ this.startedAt = startedAt;
49
+ this.events = [];
50
+ }
51
+ this.broadcast({ type: "hello", hello });
52
+ void this.backfill();
53
+ });
54
+ }
55
+ /** Uptime the connected process started at; identifies the session. */
56
+ startedAt;
57
+ /** Everything we have seen, newest last. Used by the `timeline` tool too. */
58
+ buffer() {
59
+ return this.events;
60
+ }
61
+ record(event) {
62
+ this.events.push(event);
63
+ if (this.events.length > BUFFER_LIMIT) {
64
+ this.events.splice(0, this.events.length - BUFFER_LIMIT);
65
+ }
66
+ this.broadcast({ type: "event", event });
67
+ }
68
+ /**
69
+ * Pulls the device's own ring after a (re)connect, so events emitted while
70
+ * nothing was listening are not lost — which is most of them, since the app
71
+ * usually starts before anyone attaches.
72
+ */
73
+ async backfill() {
74
+ try {
75
+ const page = await this.device.request("timeline", {
76
+ limit: 2000,
77
+ });
78
+ const known = new Set(this.events.map((e) => e.seq));
79
+ const fresh = page.events.filter((e) => !known.has(e.seq));
80
+ if (fresh.length === 0)
81
+ return;
82
+ this.events = [...fresh, ...this.events].sort((a, b) => a.seq - b.seq).slice(-BUFFER_LIMIT);
83
+ this.broadcast({ type: "reset", events: this.events });
84
+ }
85
+ catch {
86
+ // A failed backfill is not worth surfacing; live events still flow.
87
+ }
88
+ }
89
+ async start() {
90
+ if (this.started)
91
+ return this.url();
92
+ this.started = true;
93
+ const server = http.createServer(async (req, res) => {
94
+ const path = (req.url ?? "/").split("?")[0];
95
+ // The inspector proxies to the device rather than holding a copy: the
96
+ // app's tables are the app's, and a cached mirror would go stale the
97
+ // moment it mattered.
98
+ if (path.startsWith("/api/db/")) {
99
+ const query = new URL(req.url ?? "/", "http://localhost");
100
+ const method = path === "/api/db/tables"
101
+ ? "db_tables"
102
+ : path === "/api/db/rows"
103
+ ? "db_rows"
104
+ : "db_query";
105
+ const params = {
106
+ database: query.searchParams.get("database") ?? undefined,
107
+ table: query.searchParams.get("table") ?? undefined,
108
+ sql: query.searchParams.get("sql") ?? undefined,
109
+ limit: numberParam(query.searchParams.get("limit")),
110
+ offset: numberParam(query.searchParams.get("offset")),
111
+ count: query.searchParams.get("count") === "0" ? false : undefined,
112
+ };
113
+ try {
114
+ const result = await this.device.request(method, params);
115
+ res.writeHead(200, { "content-type": "application/json" });
116
+ res.end(JSON.stringify(result));
117
+ }
118
+ catch (error) {
119
+ res.writeHead(503, { "content-type": "application/json" });
120
+ res.end(JSON.stringify({ error: error.message }));
121
+ }
122
+ return;
123
+ }
124
+ if (path === "/api/tools/restart" && req.method === "POST") {
125
+ const packageName = this.device.hello?.packageName;
126
+ if (!packageName) {
127
+ res.writeHead(409, { "content-type": "application/json" });
128
+ res.end(JSON.stringify({ ok: false, output: "The app has not said hello yet." }));
129
+ return;
130
+ }
131
+ const result = restartApp(packageName, this.serial);
132
+ res.writeHead(result.ok ? 200 : 502, { "content-type": "application/json" });
133
+ res.end(JSON.stringify(result));
134
+ return;
135
+ }
136
+ // What the app wired up, and what it has on its classpath but did not.
137
+ // The UI uses it to tell an empty lane apart from a missing integration.
138
+ if (path === "/api/setup") {
139
+ try {
140
+ const result = await this.device.request("setup", {});
141
+ res.writeHead(200, { "content-type": "application/json" });
142
+ res.end(JSON.stringify(result));
143
+ }
144
+ catch (error) {
145
+ res.writeHead(503, { "content-type": "application/json" });
146
+ res.end(JSON.stringify({ error: error.message }));
147
+ }
148
+ return;
149
+ }
150
+ if (path === "/api/events") {
151
+ res.writeHead(200, { "content-type": "application/json" });
152
+ res.end(JSON.stringify({ events: this.events, hello: this.device.hello }));
153
+ return;
154
+ }
155
+ // The UI is a built Vite bundle: an entry document plus hashed assets.
156
+ // Anything that is not a real file falls back to index.html, so a deep
157
+ // link still boots the app rather than 404ing.
158
+ const requested = path === "/" ? "index.html" : path.replace(/^\/+/, "");
159
+ const resolved = resolve(UI_DIR, requested);
160
+ const target = resolved.startsWith(resolve(UI_DIR))
161
+ ? resolved
162
+ : resolve(UI_DIR, "index.html");
163
+ try {
164
+ const body = await readFile(target);
165
+ res.writeHead(200, {
166
+ "content-type": CONTENT_TYPES[extname(target)] ?? "application/octet-stream",
167
+ });
168
+ res.end(body);
169
+ }
170
+ catch {
171
+ try {
172
+ const html = await readFile(resolve(UI_DIR, "index.html"));
173
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
174
+ res.end(html);
175
+ }
176
+ catch (error) {
177
+ res.writeHead(500, { "content-type": "text/plain" });
178
+ res.end("The timeline UI has not been built. Run `npm run build` in the package root.\n" +
179
+ error.message);
180
+ }
181
+ }
182
+ });
183
+ this.wss = new WebSocketServer({ server, path: "/ws" });
184
+ this.wss.on("connection", (socket) => {
185
+ socket.send(JSON.stringify({
186
+ type: "init",
187
+ state: this.device.state,
188
+ hello: this.device.hello,
189
+ events: this.events,
190
+ }));
191
+ });
192
+ await new Promise((resolve, reject) => {
193
+ server.once("error", reject);
194
+ // Loopback only. The UI is a dev tool and has no business being routable.
195
+ server.listen(this.port, "127.0.0.1", () => {
196
+ server.removeListener("error", reject);
197
+ resolve();
198
+ });
199
+ });
200
+ this.server = server;
201
+ return this.url();
202
+ }
203
+ stop() {
204
+ this.wss?.close();
205
+ this.server?.close();
206
+ this.server = null;
207
+ this.wss = null;
208
+ this.started = false;
209
+ }
210
+ isRunning() {
211
+ return this.started;
212
+ }
213
+ url() {
214
+ return `http://127.0.0.1:${this.port}/`;
215
+ }
216
+ broadcast(message) {
217
+ if (!this.wss)
218
+ return;
219
+ const payload = JSON.stringify(message);
220
+ for (const client of this.wss.clients) {
221
+ if (client.readyState === WebSocket.OPEN)
222
+ client.send(payload);
223
+ }
224
+ }
225
+ }
226
+ function numberParam(value) {
227
+ if (value === null)
228
+ return undefined;
229
+ const parsed = Number(value);
230
+ return Number.isFinite(parsed) ? parsed : undefined;
231
+ }
232
+ //# sourceMappingURL=timeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.js","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"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"}