@pi-unipi/unipi 2.10.2 → 2.12.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/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/package.json +23 -23
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/background-tasks/package.json +2 -2
- package/packages/btw/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/core/package.json +1 -1
- package/packages/footer/README.md +20 -3
- package/packages/footer/package.json +2 -2
- package/packages/footer/src/commands.ts +9 -15
- package/packages/footer/src/config.ts +2 -0
- package/packages/footer/src/glance-editor.ts +285 -0
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +360 -27
- package/packages/footer/src/presets.ts +16 -1
- package/packages/footer/src/rendering/icons.ts +8 -0
- package/packages/footer/src/rendering/theme.ts +2 -0
- package/packages/footer/src/segments/core.ts +114 -7
- package/packages/footer/src/tps-tracker.ts +529 -240
- package/packages/footer/src/tui/settings-tui.ts +18 -2
- package/packages/footer/src/types.ts +4 -0
- package/packages/image/package.json +2 -2
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/milestone/package.json +2 -2
- package/packages/notify/package.json +2 -2
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +2 -2
- package/packages/trajectory/README.md +23 -7
- package/packages/trajectory/index.ts +23 -3
- package/packages/trajectory/package.json +1 -1
- package/packages/trajectory/src/capture.ts +150 -30
- package/packages/trajectory/src/prefix-integrity.ts +213 -0
- package/packages/trajectory/src/project.ts +57 -5
- package/packages/trajectory/src/server.ts +10 -3
- package/packages/trajectory/src/telemetry.ts +79 -18
- package/packages/trajectory/src/tracer.ts +401 -0
- package/packages/trajectory/src/ui.ts +14 -12
- package/packages/unipi/bundled.js +10080 -3406
- package/packages/unipi/index.ts +25 -22
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { TelemetrySidecar, type TelemetryEvent } from "./telemetry.js";
|
|
5
|
+
|
|
6
|
+
export interface UnipiTraceRecorder {
|
|
7
|
+
bind(sessionId: string): void;
|
|
8
|
+
record(data: unknown, extra?: Record<string, unknown>): void;
|
|
9
|
+
read(): TelemetryEvent[];
|
|
10
|
+
revision(): number;
|
|
11
|
+
cursor(): number;
|
|
12
|
+
since(cursor: number): TelemetryEvent[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UnipiTracer {
|
|
16
|
+
scope(packageName: string): ExtensionAPI;
|
|
17
|
+
recorder: UnipiTraceRecorder;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function canonicalize(value: unknown, seen = new WeakSet<object>()): unknown {
|
|
21
|
+
if (value === undefined) return { $type: "undefined" };
|
|
22
|
+
if (typeof value === "bigint") return { $type: "bigint", value: value.toString() };
|
|
23
|
+
if (typeof value === "number" && !Number.isFinite(value)) return { $type: "number", value: String(value) };
|
|
24
|
+
if (typeof value !== "object" || value === null) return value;
|
|
25
|
+
if (seen.has(value)) return { $type: "cycle" };
|
|
26
|
+
seen.add(value);
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
const result = value.map(item => canonicalize(item, seen));
|
|
29
|
+
seen.delete(value);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
if (value instanceof Uint8Array) {
|
|
33
|
+
seen.delete(value);
|
|
34
|
+
return { $type: "bytes", length: value.byteLength };
|
|
35
|
+
}
|
|
36
|
+
const result: Record<string, unknown> = {};
|
|
37
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
38
|
+
result[key] = canonicalize((value as Record<string, unknown>)[key], seen);
|
|
39
|
+
}
|
|
40
|
+
seen.delete(value);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mutationSurface(event: unknown): unknown {
|
|
45
|
+
if (!event || typeof event !== "object") return undefined;
|
|
46
|
+
const value = event as Record<string, unknown>;
|
|
47
|
+
if (value.type === "context") return value.messages;
|
|
48
|
+
if (value.type === "before_provider_request") return value.payload;
|
|
49
|
+
if (value.type === "before_provider_headers") return value.headers;
|
|
50
|
+
if (value.type === "before_agent_start") return { systemPrompt: value.systemPrompt };
|
|
51
|
+
if (value.type === "input") return { text: value.text, images: value.images };
|
|
52
|
+
if (value.type === "message_end") return value.message;
|
|
53
|
+
if (value.type === "tool_call") return value.input;
|
|
54
|
+
if (value.type === "tool_result") {
|
|
55
|
+
return { content: value.content, details: value.details, isError: value.isError, usage: value.usage };
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fingerprint(value: unknown): string {
|
|
61
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sample(value: unknown): unknown {
|
|
65
|
+
if (typeof value === "string") return value.length > 500 ? `${value.slice(0, 500)}…[truncated]` : value;
|
|
66
|
+
const text = JSON.stringify(value);
|
|
67
|
+
return text.length > 1_000 ? `${text.slice(0, 1_000)}…[truncated]` : value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function firstDifference(before: unknown, after: unknown, path = "$", depth = 0): Record<string, unknown> | undefined {
|
|
71
|
+
if (Object.is(before, after)) return undefined;
|
|
72
|
+
if (depth >= 20) return { path, kind: "changed", before: sample(before), after: sample(after) };
|
|
73
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
74
|
+
const count = Math.max(before.length, after.length);
|
|
75
|
+
for (let index = 0; index < count; index++) {
|
|
76
|
+
if (index >= before.length) return { path: `${path}[${index}]`, kind: "inserted", after: sample(after[index]) };
|
|
77
|
+
if (index >= after.length) return { path: `${path}[${index}]`, kind: "removed", before: sample(before[index]) };
|
|
78
|
+
const difference = firstDifference(before[index], after[index], `${path}[${index}]`, depth + 1);
|
|
79
|
+
if (difference) return difference;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
if (before && after && typeof before === "object" && typeof after === "object") {
|
|
84
|
+
const beforeRecord = before as Record<string, unknown>;
|
|
85
|
+
const afterRecord = after as Record<string, unknown>;
|
|
86
|
+
for (const key of [...new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)])].sort()) {
|
|
87
|
+
if (!(key in beforeRecord)) return { path: `${path}.${key}`, kind: "inserted", after: sample(afterRecord[key]) };
|
|
88
|
+
if (!(key in afterRecord)) return { path: `${path}.${key}`, kind: "removed", before: sample(beforeRecord[key]) };
|
|
89
|
+
const difference = firstDifference(beforeRecord[key], afterRecord[key], `${path}.${key}`, depth + 1);
|
|
90
|
+
if (difference) return difference;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return { path, kind: "changed", before: sample(before), after: sample(after) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mutationEvidence(before: unknown, after: unknown): Record<string, unknown> | undefined {
|
|
98
|
+
if (before === undefined || after === undefined) return undefined;
|
|
99
|
+
const beforeFingerprint = fingerprint(before);
|
|
100
|
+
const afterFingerprint = fingerprint(after);
|
|
101
|
+
return {
|
|
102
|
+
changed: beforeFingerprint !== afterFingerprint,
|
|
103
|
+
beforeFingerprint,
|
|
104
|
+
afterFingerprint,
|
|
105
|
+
...(beforeFingerprint === afterFingerprint ? {} : { firstDifference: firstDifference(before, after) }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function effectiveSurface(eventName: string, event: unknown, result: unknown): unknown {
|
|
110
|
+
const current = mutationSurface(event);
|
|
111
|
+
if (!result || typeof result !== "object") {
|
|
112
|
+
return eventName === "before_provider_request" && result !== undefined ? result : current;
|
|
113
|
+
}
|
|
114
|
+
const value = result as Record<string, unknown>;
|
|
115
|
+
if (eventName === "context" && value.messages !== undefined) return value.messages;
|
|
116
|
+
if (eventName === "before_provider_request") return result;
|
|
117
|
+
if (eventName === "before_agent_start" && value.systemPrompt !== undefined) return { systemPrompt: value.systemPrompt };
|
|
118
|
+
if (eventName === "input" && value.action === "transform") return { text: value.text, images: value.images };
|
|
119
|
+
if (eventName === "message_end" && value.message !== undefined) return value.message;
|
|
120
|
+
if (eventName === "tool_result" && current && typeof current === "object") {
|
|
121
|
+
return { ...(current as Record<string, unknown>), ...value };
|
|
122
|
+
}
|
|
123
|
+
return current;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resultEvidence(result: unknown): Record<string, unknown> | undefined {
|
|
127
|
+
if (result === undefined) return undefined;
|
|
128
|
+
const canonical = canonicalize(result);
|
|
129
|
+
return { fingerprint: fingerprint(canonical), preview: sample(canonical) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const CONTEXT_API_ACTIONS = new Set([
|
|
133
|
+
"sendMessage", "sendUserMessage", "setActiveTools", "setModel", "setThinkingLevel",
|
|
134
|
+
"registerProvider", "unregisterProvider",
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
function affectsContext(data: Record<string, unknown>): boolean {
|
|
138
|
+
if (data.phase !== "exit") return false;
|
|
139
|
+
if (data.surface === "hook") {
|
|
140
|
+
return Boolean(data.mutation && (data.mutation as Record<string, unknown>).changed === true);
|
|
141
|
+
}
|
|
142
|
+
if (data.surface === "api") return CONTEXT_API_ACTIONS.has(String(data.action));
|
|
143
|
+
if (data.surface === "context-api") {
|
|
144
|
+
return ["compact", "navigateTree", "switchSession", "newSession", "fork", "reload"].includes(String(data.action));
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function traceArgs(action: string, args: unknown[]): unknown {
|
|
150
|
+
switch (action) {
|
|
151
|
+
case "registerCommand": return { name: args[0] };
|
|
152
|
+
case "registerShortcut": return { shortcut: args[0] };
|
|
153
|
+
case "registerFlag": return { name: args[0], options: args[1] };
|
|
154
|
+
case "registerMessageRenderer": return { customType: args[0] };
|
|
155
|
+
case "registerEntryRenderer": return { customType: args[0] };
|
|
156
|
+
case "registerMarkdownTransformer": return {};
|
|
157
|
+
case "sendMessage": return { message: args[0], options: args[1] };
|
|
158
|
+
case "sendUserMessage": return { content: args[0], options: args[1] };
|
|
159
|
+
case "appendEntry": return { customType: args[0], data: args[1] };
|
|
160
|
+
case "setSessionName": return { name: args[0] };
|
|
161
|
+
case "setLabel": return { entryId: args[0], label: args[1] };
|
|
162
|
+
case "setActiveTools": return { toolNames: args[0] };
|
|
163
|
+
case "setModel": return { model: args[0] };
|
|
164
|
+
case "setThinkingLevel": return { level: args[0] };
|
|
165
|
+
case "registerTool": {
|
|
166
|
+
const tool = args[0] as Record<string, unknown> | undefined;
|
|
167
|
+
return { name: tool?.name, description: tool?.description, parameters: tool?.parameters };
|
|
168
|
+
}
|
|
169
|
+
case "registerProvider": return { name: typeof args[0] === "string" ? args[0] : (args[0] as Record<string, unknown> | undefined)?.name };
|
|
170
|
+
case "unregisterProvider": return { name: args[0] };
|
|
171
|
+
default: return args;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const TRACED_ACTIONS = new Set([
|
|
176
|
+
"registerFlag", "registerMessageRenderer", "registerEntryRenderer", "registerMarkdownTransformer",
|
|
177
|
+
"sendMessage", "sendUserMessage", "appendEntry", "setSessionName", "setLabel",
|
|
178
|
+
"setActiveTools", "setModel", "setThinkingLevel", "registerProvider", "unregisterProvider",
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const CONTEXT_ACTIONS = new Set([
|
|
182
|
+
"abort", "shutdown", "compact", "newSession", "fork", "navigateTree", "switchSession", "reload",
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
function errorData(error: unknown): unknown {
|
|
186
|
+
return error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : String(error);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function scopedContext(ctx: unknown, packageName: string, recorder: UnipiTraceRecorder): unknown {
|
|
190
|
+
if (!ctx || typeof ctx !== "object") return ctx;
|
|
191
|
+
return new Proxy(ctx as Record<string, unknown>, {
|
|
192
|
+
get(target, property, receiver) {
|
|
193
|
+
const original = Reflect.get(target, property, receiver);
|
|
194
|
+
if (typeof property !== "string" || !CONTEXT_ACTIONS.has(property) || typeof original !== "function") return original;
|
|
195
|
+
return (...args: unknown[]) => {
|
|
196
|
+
const started = performance.now();
|
|
197
|
+
recorder.record({ package: packageName, surface: "context-api", phase: "enter", action: property, input: args });
|
|
198
|
+
try {
|
|
199
|
+
const result = original.apply(target, args);
|
|
200
|
+
recorder.record({ package: packageName, surface: "context-api", phase: "exit", action: property, result: resultEvidence(result), durationMs: performance.now() - started });
|
|
201
|
+
return result;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
recorder.record({ package: packageName, surface: "context-api", phase: "error", action: property, durationMs: performance.now() - started, error: errorData(error) });
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createUnipiTracer(pi: ExtensionAPI, sidecarRoot?: string): UnipiTracer {
|
|
212
|
+
let sidecar: TelemetrySidecar | null = null;
|
|
213
|
+
let sessionId: string | null = null;
|
|
214
|
+
let traceSeq = 0;
|
|
215
|
+
const recent: Array<{ seq: number; event: TelemetryEvent }> = [];
|
|
216
|
+
const pending: Array<{ data: unknown; extra: Record<string, unknown>; at: number }> = [];
|
|
217
|
+
const retain = (event: TelemetryEvent) => {
|
|
218
|
+
recent.push({ seq: traceSeq++, event });
|
|
219
|
+
if (recent.length > 2_000) recent.splice(0, recent.length - 2_000);
|
|
220
|
+
};
|
|
221
|
+
const persist = (data: unknown, extra: Record<string, unknown>, at: number) => {
|
|
222
|
+
const event = { v: 1 as const, type: "unipi-trace" as const, at, ...extra, data };
|
|
223
|
+
retain(event);
|
|
224
|
+
sidecar?.append({ type: event.type, at: event.at, ...extra, data });
|
|
225
|
+
};
|
|
226
|
+
const recorder: UnipiTraceRecorder = {
|
|
227
|
+
bind(nextSessionId) {
|
|
228
|
+
if (sessionId === nextSessionId && sidecar) return;
|
|
229
|
+
sessionId = nextSessionId;
|
|
230
|
+
sidecar = new TelemetrySidecar(nextSessionId, sidecarRoot);
|
|
231
|
+
recent.length = 0;
|
|
232
|
+
traceSeq = 0;
|
|
233
|
+
for (const event of pending.splice(0)) persist(event.data, event.extra, event.at);
|
|
234
|
+
},
|
|
235
|
+
record(data, extra = {}) {
|
|
236
|
+
const tagged = data && typeof data === "object"
|
|
237
|
+
? { ...(data as Record<string, unknown>), affectsContext: affectsContext(data as Record<string, unknown>) }
|
|
238
|
+
: data;
|
|
239
|
+
if (!sidecar) {
|
|
240
|
+
pending.push({ data: tagged, extra, at: Date.now() });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
persist(tagged, extra, Date.now());
|
|
244
|
+
},
|
|
245
|
+
read() { return sidecar?.read() ?? []; },
|
|
246
|
+
revision() { return sidecar?.revision() ?? traceSeq; },
|
|
247
|
+
cursor() { return traceSeq; },
|
|
248
|
+
since(cursor) { return recent.filter(item => item.seq >= cursor).map(item => item.event); },
|
|
249
|
+
};
|
|
250
|
+
const cache = new Map<string, ExtensionAPI>();
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
recorder,
|
|
254
|
+
scope(packageName) {
|
|
255
|
+
const cached = cache.get(packageName);
|
|
256
|
+
if (cached) return cached;
|
|
257
|
+
const events = {
|
|
258
|
+
emit(channel: string, data: unknown) {
|
|
259
|
+
recorder.record({ package: packageName, surface: "event-bus", phase: "emit", channel, data });
|
|
260
|
+
return pi.events.emit(channel, data);
|
|
261
|
+
},
|
|
262
|
+
on(channel: string, handler: (data: unknown) => void) {
|
|
263
|
+
return pi.events.on(channel, (data) => {
|
|
264
|
+
const started = performance.now();
|
|
265
|
+
recorder.record({ package: packageName, surface: "event-bus", phase: "enter", channel, data });
|
|
266
|
+
try {
|
|
267
|
+
const result = (handler as (value: unknown) => unknown)(data);
|
|
268
|
+
if (result && typeof result === "object" && "then" in result) {
|
|
269
|
+
void Promise.resolve(result).then((value) => {
|
|
270
|
+
recorder.record({ package: packageName, surface: "event-bus", phase: "exit", channel, result: resultEvidence(value), durationMs: performance.now() - started });
|
|
271
|
+
}, (error) => {
|
|
272
|
+
recorder.record({ package: packageName, surface: "event-bus", phase: "error", channel, durationMs: performance.now() - started, error: errorData(error) });
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
recorder.record({ package: packageName, surface: "event-bus", phase: "exit", channel, result: resultEvidence(result), durationMs: performance.now() - started });
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
recorder.record({ package: packageName, surface: "event-bus", phase: "error", channel, durationMs: performance.now() - started, error: errorData(error) });
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
const scoped = new Proxy(pi as ExtensionAPI & Record<string, unknown>, {
|
|
285
|
+
get(target, property, receiver) {
|
|
286
|
+
if (property === "events") return events;
|
|
287
|
+
const original = Reflect.get(target, property, receiver);
|
|
288
|
+
if (property === "on" && typeof original === "function") {
|
|
289
|
+
return (eventName: string, handler: (event: unknown, ctx: unknown) => unknown) => original.call(target, eventName, async (event: unknown, ctx: unknown) => {
|
|
290
|
+
if (eventName === "session_start") {
|
|
291
|
+
const sessionId = (ctx as { sessionManager?: { getSessionId?(): string } })?.sessionManager?.getSessionId?.();
|
|
292
|
+
if (sessionId) recorder.bind(sessionId);
|
|
293
|
+
}
|
|
294
|
+
const started = performance.now();
|
|
295
|
+
const before = canonicalize(mutationSurface(event));
|
|
296
|
+
recorder.record({ package: packageName, surface: "hook", phase: "enter", hook: eventName, inputFingerprint: before === undefined ? undefined : fingerprint(before) });
|
|
297
|
+
try {
|
|
298
|
+
const result = await handler(event, scopedContext(ctx, packageName, recorder));
|
|
299
|
+
const after = canonicalize(effectiveSurface(eventName, event, result));
|
|
300
|
+
recorder.record({ package: packageName, surface: "hook", phase: "exit", hook: eventName, result: resultEvidence(result), mutation: mutationEvidence(before, after), durationMs: performance.now() - started });
|
|
301
|
+
return result;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
recorder.record({ package: packageName, surface: "hook", phase: "error", hook: eventName, durationMs: performance.now() - started, error: errorData(error) });
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (property === "registerTool" && typeof original === "function") {
|
|
309
|
+
return (tool: Record<string, unknown>) => {
|
|
310
|
+
const wrapped = typeof tool.execute === "function" ? {
|
|
311
|
+
...tool,
|
|
312
|
+
execute: async (...args: unknown[]) => {
|
|
313
|
+
const started = performance.now();
|
|
314
|
+
recorder.record({ package: packageName, surface: "tool", phase: "enter", action: "execute", tool: tool.name, toolCallId: args[0], input: args[1] });
|
|
315
|
+
try {
|
|
316
|
+
const result = await (tool.execute as (...args: unknown[]) => unknown)(...args.slice(0, 4), scopedContext(args[4], packageName, recorder));
|
|
317
|
+
recorder.record({ package: packageName, surface: "tool", phase: "exit", action: "execute", tool: tool.name, toolCallId: args[0], result: resultEvidence(result), durationMs: performance.now() - started });
|
|
318
|
+
return result;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
recorder.record({ package: packageName, surface: "tool", phase: "error", action: "execute", tool: tool.name, toolCallId: args[0], durationMs: performance.now() - started, error: errorData(error) });
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
} : tool;
|
|
325
|
+
recorder.record({ package: packageName, surface: "api", phase: "enter", action: property, input: traceArgs(property, [tool]) });
|
|
326
|
+
const result = original.call(target, wrapped);
|
|
327
|
+
recorder.record({ package: packageName, surface: "api", phase: "exit", action: property });
|
|
328
|
+
return result;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (property === "registerCommand" && typeof original === "function") {
|
|
332
|
+
return (name: string, options: Record<string, unknown>) => {
|
|
333
|
+
const wrapped = typeof options.handler === "function" ? {
|
|
334
|
+
...options,
|
|
335
|
+
handler: async (args: string, ctx: unknown) => {
|
|
336
|
+
const started = performance.now();
|
|
337
|
+
recorder.record({ package: packageName, surface: "command", phase: "enter", action: name, input: { args } });
|
|
338
|
+
try {
|
|
339
|
+
const result = await (options.handler as (args: string, ctx: unknown) => unknown)(args, scopedContext(ctx, packageName, recorder));
|
|
340
|
+
recorder.record({ package: packageName, surface: "command", phase: "exit", action: name, result: resultEvidence(result), durationMs: performance.now() - started });
|
|
341
|
+
return result;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
recorder.record({ package: packageName, surface: "command", phase: "error", action: name, durationMs: performance.now() - started, error: errorData(error) });
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
} : options;
|
|
348
|
+
recorder.record({ package: packageName, surface: "api", phase: "enter", action: property, input: { name } });
|
|
349
|
+
const result = original.call(target, name, wrapped);
|
|
350
|
+
recorder.record({ package: packageName, surface: "api", phase: "exit", action: property });
|
|
351
|
+
return result;
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (property === "registerShortcut" && typeof original === "function") {
|
|
355
|
+
return (shortcut: unknown, options: Record<string, unknown>) => original.call(target, shortcut, typeof options.handler === "function" ? {
|
|
356
|
+
...options,
|
|
357
|
+
handler: async (ctx: unknown) => {
|
|
358
|
+
const started = performance.now();
|
|
359
|
+
recorder.record({ package: packageName, surface: "shortcut", phase: "enter", action: String(shortcut) });
|
|
360
|
+
try {
|
|
361
|
+
const result = await (options.handler as (ctx: unknown) => unknown)(scopedContext(ctx, packageName, recorder));
|
|
362
|
+
recorder.record({ package: packageName, surface: "shortcut", phase: "exit", action: String(shortcut), result: resultEvidence(result), durationMs: performance.now() - started });
|
|
363
|
+
return result;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
recorder.record({ package: packageName, surface: "shortcut", phase: "error", action: String(shortcut), durationMs: performance.now() - started, error: errorData(error) });
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
} : options);
|
|
370
|
+
}
|
|
371
|
+
if (typeof property === "string" && TRACED_ACTIONS.has(property) && typeof original === "function") {
|
|
372
|
+
return (...args: unknown[]) => {
|
|
373
|
+
const started = performance.now();
|
|
374
|
+
recorder.record({ package: packageName, surface: "api", phase: "enter", action: property, input: traceArgs(property, args) });
|
|
375
|
+
try {
|
|
376
|
+
const result = original.apply(target, args);
|
|
377
|
+
if (result && typeof result === "object" && "then" in result) {
|
|
378
|
+
return Promise.resolve(result).then((value) => {
|
|
379
|
+
recorder.record({ package: packageName, surface: "api", phase: "exit", action: property, result: value, durationMs: performance.now() - started });
|
|
380
|
+
return value;
|
|
381
|
+
}, (error) => {
|
|
382
|
+
recorder.record({ package: packageName, surface: "api", phase: "error", action: property, durationMs: performance.now() - started, error: errorData(error) });
|
|
383
|
+
throw error;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
recorder.record({ package: packageName, surface: "api", phase: "exit", action: property, result, durationMs: performance.now() - started });
|
|
387
|
+
return result;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
recorder.record({ package: packageName, surface: "api", phase: "error", action: property, durationMs: performance.now() - started, error: errorData(error) });
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return typeof original === "function" ? original.bind(target) : original;
|
|
395
|
+
},
|
|
396
|
+
}) as ExtensionAPI;
|
|
397
|
+
cache.set(packageName, scoped);
|
|
398
|
+
return scoped;
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
@@ -5,6 +5,8 @@ export const TRAJECTORY_HTML = `<!doctype html>
|
|
|
5
5
|
<button id="duration" class="toggle" type="button" aria-pressed="false">◷ Duration</button>
|
|
6
6
|
<button id="turns" class="toggle" type="button" aria-pressed="false">Turns</button>
|
|
7
7
|
<button id="calls" class="toggle" type="button" aria-pressed="false">Calls</button>
|
|
8
|
+
<button id="unipi-only" class="toggle" type="button" aria-pressed="false">UniPi</button>
|
|
9
|
+
<button id="violations" class="toggle" type="button" aria-pressed="false">Violations</button>
|
|
8
10
|
<span class="spacer"></span><span id="connection" class="connection">UniPi · Live</span>
|
|
9
11
|
<button id="theme" class="theme-toggle" type="button" aria-label="Toggle dark theme" title="Toggle theme">☾</button>
|
|
10
12
|
<label class="search"><span>⌕</span><input id="search" type="search" aria-label="Search trajectory" placeholder="Search trajectory"></label>
|
|
@@ -15,7 +17,7 @@ export const TRAJECTORY_HTML = `<!doctype html>
|
|
|
15
17
|
<script type="module" src="/app.js"></script></body></html>`;
|
|
16
18
|
|
|
17
19
|
export const TRAJECTORY_CSS = `
|
|
18
|
-
:root{color-scheme:light;--bg:#f7f8fa;--panel:#fff;--line:#e4e6eb;--muted:#74777d;--text:#202124;--blue:#3977ef;--user:#4d8bf5;--model:#9b70df;--tool:#e39b45;--error:#dc4b4b;font:13px Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text)}*{box-sizing:border-box}body{margin:0;overflow:hidden}button,input{font:inherit}.product-nav{height:34px;display:flex;align-items:center;gap:20px;padding:0 20px;border-bottom:1px solid var(--line);background:var(--panel);font-size:12px}.product-nav strong{font-weight:600}.product-nav span{height:34px;display:flex;align-items:center;border-bottom:2px solid var(--blue);color:var(--blue)}.toolbar{height:32px;display:flex;align-items:center;gap:10px;padding:0 20px;background:var(--panel);border-bottom:1px solid var(--line)}button{border:1px solid var(--line);background:#fff;border-radius:4px;color:#50535a;padding:3px 8px;cursor:pointer}.toggle{border-color:transparent;background:transparent;padding:2px}.toggle.active{background:#edf3ff;border-color:#b9cdf7;color:#245ec8}.spacer{flex:1}.connection{font-size:11px;color:var(--muted)}.connection.live{color:#27865b}.search{display:flex;align-items:center;gap:6px;width:min(280px,32vw);border:1px solid var(--line);background:#fafbfc;border-radius:6px;padding:5px 9px}.search input{width:100%;border:0;outline:0;background:transparent}.timeline{height:84px;display:grid;grid-template-columns:56px 1fr;background:var(--panel);border-bottom:1px solid var(--line)}.lane-labels{position:relative;border-right:1px solid var(--line);color:var(--muted);font-size:10px;line-height:1}.lane-labels span{position:absolute;right:5px;height:12px}.lane-labels span:nth-child(1){top:12px}.lane-labels span:nth-child(2){top:35px}.lane-labels span:nth-child(3){top:58px}.timeline-track{position:relative;overflow:hidden;cursor:crosshair}.span{position:absolute;height:12px;min-width:2px;border-radius:1px;opacity:.9;transition:opacity .1s,filter .1s}.span:hover,.span.selected{opacity:1;filter:brightness(.92);z-index:3}.span.user{top:12px;background:var(--user)}.span.assistant{top:35px;background:var(--model)}.span.tool{top:58px;background:var(--tool)}.span.compaction,.span.branch{top:35px;background:#8b919d}.span.error{background:var(--error)}.range{position:absolute;inset:0 auto 0 0;background:#3977ef1c;border-left:1px solid var(--blue);border-right:1px solid var(--blue);pointer-events:none;z-index:2}.workspace{height:calc(100vh - 150px);display:grid;grid-template-columns:minmax(360px,1fr) minmax(340px,38%)}.workspace.details-closed{grid-template-columns:1fr}.ledger-pane{overflow:auto;background:var(--panel)}table{width:100%;border-collapse:collapse;table-layout:fixed}tr{border-bottom:1px solid #eceef1;cursor:pointer}tr.turn-start{border-top:3px solid #cfd3da}tr:hover{background:#f6f8fc}tr.selected{background:#edf3ff}tr.filtered,tr.timeline-filtered{display:none}td{height:30px;vertical-align:middle;padding:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.event-column{width:172px}.event{position:relative;overflow:visible!important;color:#555a63;text-align:right;padding-left:76px!important}.event::before{content:'•';position:absolute;left:13px;color:#81858d}.request-boundary{position:absolute;top:3px;left:20px;display:inline-grid;align-items:center;height:14px;max-width:68px;padding:0 5px;border:1px solid var(--line);border-radius:2px;background:var(--panel);color:var(--muted);font:9px/12px ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap}.content{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:700;letter-spacing:.045em}.badge.user{color:var(--user)}.badge.assistant{color:var(--model)}.badge.tool{color:#b87322}.badge.compaction,.badge.branch{color:#6f7580}.request{display:inline-block;margin-right:6px;padding:2px 5px;border:1px solid #d7d9df;border-radius:4px;color:#666;font-weight:500;letter-spacing:0}.location{display:none}.content{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.4}.arrow{color:#a3a6ad;margin:0 6px}.result{color:#656a73}.error-text{color:var(--error)}.empty{display:none;padding:50px;text-align:center;color:var(--muted)}.empty.visible{display:block}.details{border-left:1px solid var(--line);background:var(--panel);overflow:hidden}.details>header{height:56px;display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--line)}.details>header div{display:flex;flex-direction:column;gap:3px}.details small{color:var(--muted)}#detail-close{font-size:18px;border:0}.tabs{display:flex;gap:2px;padding:8px 12px 0;border-bottom:1px solid var(--line);overflow:auto}.tabs button{border:0;border-bottom:2px solid transparent;border-radius:0;background:none;padding:8px}.tabs button.active{color:var(--blue);border-bottom-color:var(--blue)}.detail-body{height:calc(100% - 101px);overflow:auto;padding:15px}.summary{display:grid;grid-template-columns:120px 1fr;gap:8px 12px;margin:0}.summary dt{color:var(--muted)}.summary dd{margin:0;overflow-wrap:anywhere}.payload{margin:0;padding:13px;background:#f5f6f8;border:1px solid #e5e7eb;border-radius:6px;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.folded{display:none}@media(max-width:760px){.workspace{grid-template-columns:1fr}.details{position:absolute;inset:150px 0 0 10%;z-index:10;box-shadow:-8px 0 24px #0002}.event-column{width:156px}.timeline{grid-template-columns:50px 1fr}.request-boundary{left:18px}.search{width:160px}.product-nav{padding:0 10px}.toolbar{padding:0 10px}}
|
|
20
|
+
:root{color-scheme:light;--bg:#f7f8fa;--panel:#fff;--line:#e4e6eb;--muted:#74777d;--text:#202124;--blue:#3977ef;--user:#4d8bf5;--model:#9b70df;--tool:#e39b45;--error:#dc4b4b;font:13px Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text)}*{box-sizing:border-box}body{margin:0;overflow:hidden}button,input{font:inherit}.product-nav{height:34px;display:flex;align-items:center;gap:20px;padding:0 20px;border-bottom:1px solid var(--line);background:var(--panel);font-size:12px}.product-nav strong{font-weight:600}.product-nav span{height:34px;display:flex;align-items:center;border-bottom:2px solid var(--blue);color:var(--blue)}.toolbar{height:32px;display:flex;align-items:center;gap:10px;padding:0 20px;background:var(--panel);border-bottom:1px solid var(--line)}button{border:1px solid var(--line);background:#fff;border-radius:4px;color:#50535a;padding:3px 8px;cursor:pointer}.toggle{border-color:transparent;background:transparent;padding:2px}.toggle.active{background:#edf3ff;border-color:#b9cdf7;color:#245ec8}.spacer{flex:1}.connection{font-size:11px;color:var(--muted)}.connection.live{color:#27865b}.search{display:flex;align-items:center;gap:6px;width:min(280px,32vw);border:1px solid var(--line);background:#fafbfc;border-radius:6px;padding:5px 9px}.search input{width:100%;border:0;outline:0;background:transparent}.timeline{height:84px;display:grid;grid-template-columns:56px 1fr;background:var(--panel);border-bottom:1px solid var(--line)}.lane-labels{position:relative;border-right:1px solid var(--line);color:var(--muted);font-size:10px;line-height:1}.lane-labels span{position:absolute;right:5px;height:12px}.lane-labels span:nth-child(1){top:12px}.lane-labels span:nth-child(2){top:35px}.lane-labels span:nth-child(3){top:58px}.timeline-track{position:relative;overflow:hidden;cursor:crosshair}.span{position:absolute;height:12px;min-width:2px;border-radius:1px;opacity:.9;transition:opacity .1s,filter .1s}.span:hover,.span.selected{opacity:1;filter:brightness(.92);z-index:3}.span.user{top:12px;background:var(--user)}.span.assistant{top:35px;background:var(--model)}.span.tool{top:58px;background:var(--tool)}.span.system,.span.hook,.span.unipi,.span.prefix,.span.compaction,.span.branch{top:35px;background:#8b919d}.span.prefix{background:#2f9d68}.span.prefix.error{background:var(--error)}.span.error{background:var(--error)}.range{position:absolute;inset:0 auto 0 0;background:#3977ef1c;border-left:1px solid var(--blue);border-right:1px solid var(--blue);pointer-events:none;z-index:2}.workspace{height:calc(100vh - 150px);display:grid;grid-template-columns:minmax(360px,1fr) minmax(340px,38%)}.workspace.details-closed{grid-template-columns:1fr}.ledger-pane{overflow:auto;background:var(--panel)}table{width:100%;border-collapse:collapse;table-layout:fixed}tr{border-bottom:1px solid #eceef1;cursor:pointer}tr.turn-start{border-top:3px solid #cfd3da}tr:hover{background:#f6f8fc}tr.selected{background:#edf3ff}tr.filtered,tr.timeline-filtered{display:none}td{height:30px;vertical-align:middle;padding:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.event-column{width:172px}.event{position:relative;overflow:visible!important;color:#555a63;text-align:right;padding-left:76px!important}.event::before{content:'•';position:absolute;left:13px;color:#81858d}.request-boundary{position:absolute;top:3px;left:20px;display:inline-grid;align-items:center;height:14px;max-width:68px;padding:0 5px;border:1px solid var(--line);border-radius:2px;background:var(--panel);color:var(--muted);font:9px/12px ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap}.content{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:700;letter-spacing:.045em}.badge.user{color:var(--user)}.badge.assistant{color:var(--model)}.badge.tool{color:#b87322}.badge.system,.badge.hook,.badge.unipi,.badge.prefix,.badge.compaction,.badge.branch{color:#6f7580}.badge.prefix{color:#27865b}.badge.prefix.error-text{color:var(--error)}.request{display:inline-block;margin-right:6px;padding:2px 5px;border:1px solid #d7d9df;border-radius:4px;color:#666;font-weight:500;letter-spacing:0}.location{display:none}.content{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.4}.arrow{color:#a3a6ad;margin:0 6px}.result{color:#656a73}.error-text{color:var(--error)}.empty{display:none;padding:50px;text-align:center;color:var(--muted)}.empty.visible{display:block}.details{border-left:1px solid var(--line);background:var(--panel);overflow:hidden}.details>header{height:56px;display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--line)}.details>header div{display:flex;flex-direction:column;gap:3px}.details small{color:var(--muted)}#detail-close{font-size:18px;border:0}.tabs{display:flex;gap:2px;padding:8px 12px 0;border-bottom:1px solid var(--line);overflow:auto}.tabs button{border:0;border-bottom:2px solid transparent;border-radius:0;background:none;padding:8px}.tabs button.active{color:var(--blue);border-bottom-color:var(--blue)}.detail-body{height:calc(100% - 101px);overflow:auto;padding:15px}.summary{display:grid;grid-template-columns:120px 1fr;gap:8px 12px;margin:0}.summary dt{color:var(--muted)}.summary dd{margin:0;overflow-wrap:anywhere}.payload{margin:0;padding:13px;background:#f5f6f8;border:1px solid #e5e7eb;border-radius:6px;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.folded{display:none}@media(max-width:760px){.workspace{grid-template-columns:1fr}.details{position:absolute;inset:150px 0 0 10%;z-index:10;box-shadow:-8px 0 24px #0002}.event-column{width:156px}.timeline{grid-template-columns:50px 1fr}.request-boundary{left:18px}.search{width:160px}.product-nav{padding:0 10px}.toolbar{padding:0 10px}}
|
|
19
21
|
`;
|
|
20
22
|
|
|
21
23
|
export const TRAJECTORY_THEME_CSS = `
|
|
@@ -36,8 +38,8 @@ html,body{background:var(--background);color:var(--foreground);font-family:var(-
|
|
|
36
38
|
.product-nav,.toolbar{background:var(--card);border-color:var(--border)}.toolbar{height:32px;margin:0;padding:0 20px;border-width:0 0 1px;border-style:solid;border-radius:0;box-shadow:none}
|
|
37
39
|
button{background:var(--card);color:var(--card-foreground);border-color:var(--border);border-radius:var(--radius);transition:transform .12s,box-shadow .12s,background .12s}button:hover{background:color-mix(in oklch,var(--muted) 42%,var(--card));box-shadow:var(--shadow-sm)}button:active{transform:translateY(1px)}button:focus-visible,input:focus-visible,.timeline-track:focus-visible{outline:2px solid var(--ring);outline-offset:2px}
|
|
38
40
|
.toggle.active{background:var(--primary);border-color:color-mix(in oklch,var(--primary) 72%,var(--border));color:var(--primary-foreground)}.theme-toggle{width:32px;height:32px;padding:0;font-size:16px}.connection.live{color:color-mix(in oklch,var(--secondary) 80%,var(--foreground))}.search{background:color-mix(in oklch,var(--card) 78%,var(--background));border-color:var(--input);border-radius:var(--radius)}.search input{color:var(--foreground)}
|
|
39
|
-
.timeline{height:84px;margin:0;background:var(--card);border-width:0 0 1px;border-style:solid;border-color:var(--border);border-radius:0;box-shadow:none}.timeline-track{border-left-color:var(--border);border-radius:0;background:transparent}.lane-labels,.location,.details small,.summary dt{color:var(--muted-foreground)}.span.user{background:var(--trajectory-user)}.span.assistant{background:var(--trajectory-model)}.span.tool{background:var(--trajectory-tool)}.span.compaction,.span.branch{background:var(--muted)}.range{background:color-mix(in oklch,var(--secondary) 20%,transparent);border-color:var(--secondary)}
|
|
40
|
-
.workspace{height:calc(100vh - 150px);gap:0;padding:0}.ledger-pane,.details{background:var(--card);border:0;border-radius:0;box-shadow:none;overflow:auto}.details{border-left:1px solid var(--border);box-shadow:var(--shadow-lg)}tr{border-bottom-color:color-mix(in oklch,var(--border) 72%,transparent)}tr.turn-start{border-top-color:var(--secondary)}tr:hover{background:color-mix(in oklch,var(--muted) 24%,transparent)}tr.selected{background:color-mix(in oklch,var(--secondary) 18%,transparent)}.content{font-family:var(--font-mono)}.request{background:color-mix(in oklch,var(--muted) 26%,transparent);border-color:var(--border);color:var(--muted-foreground)}.badge.user{color:var(--trajectory-user)}.badge.assistant{padding:2px 5px;border-radius:3px;background:color-mix(in oklch,var(--trajectory-model) 18%,transparent);color:var(--trajectory-model)}.badge.tool{color:var(--trajectory-tool)}.result{color:var(--muted-foreground)}.error-text{color:var(--destructive)}
|
|
41
|
+
.timeline{height:84px;margin:0;background:var(--card);border-width:0 0 1px;border-style:solid;border-color:var(--border);border-radius:0;box-shadow:none}.timeline-track{border-left-color:var(--border);border-radius:0;background:transparent}.lane-labels,.location,.details small,.summary dt{color:var(--muted-foreground)}.span.user{background:var(--trajectory-user)}.span.assistant{background:var(--trajectory-model)}.span.tool{background:var(--trajectory-tool)}.span.system,.span.hook,.span.unipi,.span.compaction,.span.branch{background:var(--muted)}.span.prefix{background:color-mix(in oklch,var(--secondary) 78%,#22a06b)}.range{background:color-mix(in oklch,var(--secondary) 20%,transparent);border-color:var(--secondary)}
|
|
42
|
+
.workspace{height:calc(100vh - 150px);gap:0;padding:0}.ledger-pane,.details{background:var(--card);border:0;border-radius:0;box-shadow:none;overflow:auto}.details{border-left:1px solid var(--border);box-shadow:var(--shadow-lg)}tr{border-bottom-color:color-mix(in oklch,var(--border) 72%,transparent)}tr.turn-start{border-top-color:var(--secondary)}tr:hover{background:color-mix(in oklch,var(--muted) 24%,transparent)}tr.selected{background:color-mix(in oklch,var(--secondary) 18%,transparent)}.content{font-family:var(--font-mono)}.request{background:color-mix(in oklch,var(--muted) 26%,transparent);border-color:var(--border);color:var(--muted-foreground)}.badge.user{color:var(--trajectory-user)}.badge.assistant{padding:2px 5px;border-radius:3px;background:color-mix(in oklch,var(--trajectory-model) 18%,transparent);color:var(--trajectory-model)}.badge.tool{color:var(--trajectory-tool)}.badge.system,.badge.hook,.badge.unipi{color:var(--muted-foreground)}.badge.prefix{color:color-mix(in oklch,var(--secondary) 70%,#17834f)}.result{color:var(--muted-foreground)}.error-text{color:var(--destructive)}
|
|
41
43
|
.details>header,.tabs{border-color:var(--border)}.tabs button.active{color:var(--secondary);border-bottom-color:var(--secondary)}.payload{font-family:var(--font-mono);background:color-mix(in oklch,var(--background) 70%,var(--card));border-color:var(--border)}
|
|
42
44
|
@media(max-width:760px){.toolbar{margin:0}.timeline{margin:0}.workspace{height:calc(100vh - 150px);padding:0}.details{inset:150px 0 0 6%;border-radius:0}.connection{display:none}}
|
|
43
45
|
`;
|
|
@@ -45,23 +47,23 @@ button{background:var(--card);color:var(--card-foreground);border-color:var(--bo
|
|
|
45
47
|
export const TRAJECTORY_APP = String.raw`
|
|
46
48
|
const q=s=>document.querySelector(s), ledger=q('#ledger'), empty=q('#empty'), details=q('#details'), workspace=q('.workspace'), track=q('#timeline-track');
|
|
47
49
|
const savedTheme=localStorage.getItem('unipi-trajectory-theme');const prefersDark=matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',savedTheme?savedTheme==='dark':prefersDark);
|
|
48
|
-
let snapshot={records:[]}, selected=null, activeTab='summary', revision='', collapseTurns=false, collapseCalls=false, timelineRange=null, dragStart=null, actualDuration=false;
|
|
50
|
+
let snapshot={records:[]}, selected=null, activeTab='summary', revision='', collapseTurns=false, collapseCalls=false, unipiOnly=false, violationsOnly=false, timelineRange=null, dragStart=null, actualDuration=false;
|
|
49
51
|
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
50
52
|
const stringify=v=>typeof v==='string'?v:JSON.stringify(v??null,null,2);
|
|
51
53
|
const fmtTime=n=>n?new Date(n).toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit',second:'2-digit',fractionalSecondDigits:3}):'';
|
|
52
54
|
const fmtDuration=n=>n==null?'':n<1000?Math.round(n)+' ms':(n/1000).toFixed(n<10000?2:1)+' s';
|
|
53
|
-
const location=r=>r.turn==null?'
|
|
55
|
+
const location=r=>r.kind==='system'?'Agent run':r.kind==='unipi'?(r.package||'UniPi')+(r.surface?' · '+r.surface:''):r.kind==='prefix'?'Epoch '+(r.epoch??'?')+(r.request?.requestId!=null?' · Request '+r.request.requestId:''):r.kind==='hook'&&r.request?.requestId!=null?'Request '+r.request.requestId+(r.turn==null?'':' · Turn '+r.turn):r.turn==null?'Session':'Turn '+r.turn+(r.step==null?'':' · Step '+r.step);
|
|
54
56
|
function has(v){if(v==null)return false;if(typeof v==='string')return v.trim().length>0;if(Array.isArray(v))return v.length>0;if(typeof v==='object')return Object.keys(v).length>0;return true}
|
|
55
|
-
function tabs(r){const values=r.kind==='tool'?[['summary',r],['payload',r.input],['result',r.output],['timing',r.timing]]:r.kind==='assistant'?[['summary',r],['output',r.output],['thinking',r.thinking],['request',r.request],['tools',r.tools],['usage',r.usage],['timing',r.timing]]:r.kind==='user'?[['summary',r],['input',r.input]]:[['summary',r],['output',r.output],['usage',r.usage]];return values.filter(([tab,value])=>tab==='summary'||has(value)).map(([tab])=>tab)}
|
|
56
|
-
function detailValue(r,tab){if(tab==='payload'||tab==='input')return r.input;if(tab==='result'||tab==='output')return r.output;if(tab==='thinking')return r.thinking;if(tab==='request')return r.request;if(tab==='tools')return r.tools;if(tab==='usage')return r.usage;if(tab==='timing')return r.timing;return null}
|
|
57
|
-
function summary(r){const rows=[['Status',r.isError?'Error':r.durationMs==null&&r.kind==='tool'?'Running':'Complete'],['Started',fmtTime(r.timestamp)],['Duration',fmtDuration(r.durationMs)],['TTFT',fmtDuration(r.timing?.ttftMs)],['Decoding',fmtDuration(r.timing?.decodingMs)],['Provider',r.provider],['Model',r.model],['Stop reason',r.stopReason],['Tool',r.toolName],['Call ID',r.toolCallId],['Input tokens',r.usage?.input],['Cached tokens',r.usage?.cacheRead],['Cache created',r.usage?.cacheWrite],['Output tokens',r.usage?.output],['Reasoning tokens',r.usage?.reasoning],['Total tokens',r.usage?.totalTokens],['Cost',r.usage?.cost==null?undefined:'$'+r.usage.cost.toFixed(6)],['Tokens before compaction',r.tokensBefore]].filter(x=>has(x[1]));return '<dl class="summary">'+rows.map(x=>'<dt>'+esc(x[0])+'</dt><dd>'+esc(x[1])+'</dd>').join('')+'</dl>'}
|
|
57
|
+
function tabs(r){const values=r.kind==='tool'?[['summary',r],['payload',r.input],['result',r.output],['timing',r.timing]]:r.kind==='assistant'?[['summary',r],['output',r.output],['thinking',r.thinking],['request',r.request],['tools',r.tools],['usage',r.usage],['timing',r.timing]]:r.kind==='user'?[['summary',r],['input',r.input]]:r.kind==='system'?[['summary',r],['prompt',r.output]]:r.kind==='unipi'?[['summary',r],['trace',r.data]]:r.kind==='prefix'?[['summary',r],['integrity',r.data],['attribution',r.attribution]]:[['summary',r],['output',r.output],['usage',r.usage]];return values.filter(([tab,value])=>tab==='summary'||has(value)).map(([tab])=>tab)}
|
|
58
|
+
function detailValue(r,tab){if(tab==='payload'||tab==='input')return r.input;if(tab==='result'||tab==='output'||tab==='prompt')return r.output;if(tab==='thinking')return r.thinking;if(tab==='request')return r.request;if(tab==='tools')return r.tools;if(tab==='usage')return r.usage;if(tab==='timing')return r.timing;if(tab==='details'||tab==='event'||tab==='trace'||tab==='integrity')return r.data;if(tab==='attribution')return r.attribution;return null}
|
|
59
|
+
function summary(r){const rows=[['Status',r.isError?'Error':r.durationMs==null&&r.kind==='tool'?'Running':'Complete'],['Started',fmtTime(r.timestamp)],['Event',r.data?.name],['Package',r.package],['Surface',r.surface],['Phase',r.phase],['Verdict',r.verdict],['Epoch',r.epoch],['Request',r.request?.requestId],['Duration',fmtDuration(r.durationMs)],['TTFT',fmtDuration(r.timing?.ttftMs)],['Decoding',fmtDuration(r.timing?.decodingMs)],['Provider',r.provider],['Model',r.model],['Stop reason',r.stopReason],['Tool',r.toolName],['Call ID',r.toolCallId],['Input tokens',r.usage?.input],['Cached tokens',r.usage?.cacheRead],['Cache created',r.usage?.cacheWrite],['Output tokens',r.usage?.output],['Reasoning tokens',r.usage?.reasoning],['Total tokens',r.usage?.totalTokens],['Cost',r.usage?.cost==null?undefined:'$'+r.usage.cost.toFixed(6)],['Tokens before compaction',r.tokensBefore]].filter(x=>has(x[1]));return '<dl class="summary">'+rows.map(x=>'<dt>'+esc(x[0])+'</dt><dd>'+esc(x[1])+'</dd>').join('')+'</dl>'}
|
|
58
60
|
function showDetails(r){selected=r.id;details.hidden=false;workspace.classList.remove('details-closed');q('#detail-kind').textContent=r.title;q('#detail-location').textContent=location(r);const available=tabs(r);if(!available.includes(activeTab))activeTab='summary';q('#detail-tabs').innerHTML=available.map(t=>'<button role="tab" data-tab="'+t+'" class="'+(t===activeTab?'active':'')+'">'+esc(t[0].toUpperCase()+t.slice(1))+'</button>').join('');q('#detail-tabs').querySelectorAll('button').forEach(b=>b.onclick=()=>{activeTab=b.dataset.tab;showDetails(r)});q('#detail-body').innerHTML=activeTab==='summary'?summary(r):'<pre class="payload">'+esc(stringify(detailValue(r,activeTab)))+'</pre>';renderRows()}
|
|
59
|
-
function renderRows(){const query=q('#search').value.toLowerCase();let lastTurn=null,request=0;ledger.innerHTML='';for(const r of snapshot.records){if(r.kind==='assistant')request++;const hay=JSON.stringify(r).toLowerCase();const filtered=query&&!hay.includes(query);const timelineFiltered=timelineRange&&(r.timestamp+(r.durationMs??0)<timelineRange[0]||r.timestamp>timelineRange[1]);const foldTurn=collapseTurns&&r.turn!=null&&r.turn===lastTurn&&r.kind!=='user';const foldCall=collapseCalls&&r.kind==='tool';const tr=document.createElement('tr');tr.dataset.id=r.id;tr.dataset.kind=r.kind;tr.className=[r.turn!==lastTurn&&r.turn!=null?'turn-start':'',selected===r.id?'selected':'',filtered?'filtered':'',timelineFiltered?'timeline-filtered':'',(foldTurn||foldCall)?'folded':''].filter(Boolean).join(' ');const req=r.kind==='assistant'?'<span class="request-boundary">Request #'+request+'</span>':'';const result=r.kind==='tool'&&r.output!=null?'<span class="arrow">→</span><span class="result '+(r.isError?'error-text':'')+'">'+esc((typeof r.output==='string'?r.output:JSON.stringify(r.output)).replace(/\s+/g,' ').slice(0,180))+'</span>':'';tr.innerHTML='<td class="event">'+req+'<span class="badge '+r.kind+'">'+esc(r.title)+'</span><span class="location">'+esc(location(r))+'</span></td><td class="content">'+esc(r.preview||'')+result+'</td>';tr.onclick=()=>showDetails(r);ledger.append(tr);if(r.turn!=null)lastTurn=r.turn}empty.classList.toggle('visible',snapshot.records.length===0)}
|
|
61
|
+
function renderRows(){const query=q('#search').value.toLowerCase();let lastTurn=null,request=0;ledger.innerHTML='';for(const r of snapshot.records){if(r.kind==='assistant')request++;const hay=JSON.stringify(r).toLowerCase();const filtered=query&&!hay.includes(query)||unipiOnly&&!['unipi','prefix'].includes(r.kind)||violationsOnly&&!(r.kind==='prefix'&&r.verdict==='violation');const timelineFiltered=timelineRange&&(r.timestamp+(r.durationMs??0)<timelineRange[0]||r.timestamp>timelineRange[1]);const foldTurn=collapseTurns&&r.turn!=null&&r.turn===lastTurn&&r.kind!=='user';const foldCall=collapseCalls&&r.kind==='tool';const tr=document.createElement('tr');tr.dataset.id=r.id;tr.dataset.kind=r.kind;tr.className=[r.turn!==lastTurn&&r.turn!=null?'turn-start':'',selected===r.id?'selected':'',filtered?'filtered':'',timelineFiltered?'timeline-filtered':'',(foldTurn||foldCall)?'folded':''].filter(Boolean).join(' ');const req=r.kind==='assistant'?'<span class="request-boundary">Request #'+request+'</span>':'';const result=r.kind==='tool'&&r.output!=null?'<span class="arrow">→</span><span class="result '+(r.isError?'error-text':'')+'">'+esc((typeof r.output==='string'?r.output:JSON.stringify(r.output)).replace(/\s+/g,' ').slice(0,180))+'</span>':'';tr.innerHTML='<td class="event">'+req+'<span class="badge '+r.kind+(r.isError?' error-text':'')+'">'+esc(r.title)+'</span><span class="location">'+esc(location(r))+'</span></td><td class="content">'+esc(r.preview||'')+result+'</td>';tr.onclick=()=>showDetails(r);ledger.append(tr);if(r.turn!=null)lastTurn=r.turn}empty.classList.toggle('visible',snapshot.records.length===0)}
|
|
60
62
|
function timelineModel(){const records=snapshot.records;if(!records.length)return null;if(!actualDuration)return{min:0,max:records.length,spans:records.map((r,index)=>({r,start:index,end:index+1}))};const ordered=records.map((r,index)=>({r,index,start:r.timestamp,end:r.timestamp+Math.max(0,r.durationMs??0)})).sort((a,b)=>a.start-b.start||a.index-b.index);const offsets=new Map;let removed=0,covered=null;for(const span of ordered){if(covered!=null&&span.start>covered)removed+=span.start-covered;offsets.set(span.r.id,removed);covered=covered==null?span.end:Math.max(covered,span.end)}const spans=records.map(r=>({r,start:r.timestamp-(offsets.get(r.id)??0),end:r.timestamp+Math.max(0,r.durationMs??0)-(offsets.get(r.id)??0)}));const min=Math.min(...spans.map(x=>x.start)),rawMax=Math.max(...spans.map(x=>x.end));return{min,max:Math.max(min+1,rawMax),spans}}
|
|
61
63
|
function renderTimeline(){track.innerHTML='';const d=timelineModel();if(!d)return;for(const {r,start,end} of d.spans){const s=document.createElement('span');s.className='span '+r.kind+(r.isError?' error':'')+(selected===r.id?' selected':'');s.style.left=((start-d.min)/(d.max-d.min)*100)+'%';s.style.width=actualDuration?Math.max(.13,((end-start)/(d.max-d.min)*100))+'%':'max(2px,calc('+(100/snapshot.records.length)+'% - 2px))';s.title=r.title+(fmtTime(r.timestamp)?'\n'+fmtTime(r.timestamp):'')+(fmtDuration(r.durationMs)?' · '+fmtDuration(r.durationMs):'');s.onclick=e=>{e.stopPropagation();showDetails(r)};track.append(s)}if(timelineRange){const range=document.createElement('span');range.className='range';range.style.left=((timelineRange[0]-d.min)/(d.max-d.min)*100)+'%';range.style.width=((timelineRange[1]-timelineRange[0])/(d.max-d.min)*100)+'%';track.append(range)}}
|
|
62
64
|
function render(){q('#connection').textContent='UniPi · Live';q('#connection').className='connection live';renderRows();renderTimeline();if(selected){const r=snapshot.records.find(x=>x.id===selected);if(r)showDetails(r)}}
|
|
63
|
-
async function update(){try{const response=await fetch('/api/snapshot',{cache:'no-store'});if(!response.ok)throw Error(response.status);const
|
|
65
|
+
async function update(){try{const response=await fetch('/api/snapshot',{cache:'no-store'});if(!response.ok)throw Error(response.status);const next=response.headers.get('etag')||'';if(next&&next===revision)return;const data=await response.json();revision=next;snapshot=data;render()}catch(error){q('#connection').textContent='UniPi';q('#connection').className='connection'}}
|
|
64
66
|
function syncThemeButton(){const dark=document.documentElement.classList.contains('dark');q('#theme').textContent=dark?'☀':'☾';q('#theme').setAttribute('aria-label',dark?'Use light theme':'Use dark theme')}syncThemeButton();q('#theme').onclick=()=>{const dark=document.documentElement.classList.toggle('dark');localStorage.setItem('unipi-trajectory-theme',dark?'dark':'light');syncThemeButton()};
|
|
65
|
-
q('#detail-close').onclick=()=>{selected=null;details.hidden=true;workspace.classList.add('details-closed');renderRows();renderTimeline()};q('#search').oninput=renderRows;
|
|
66
|
-
function timelinePoint(e){const d=timelineModel(),box=track.getBoundingClientRect();return d?d.min+Math.max(0,Math.min(1,(e.clientX-box.left)/box.width))*(d.max-d.min):0}track.onpointerdown=e=>{if(e.button===2){timelineRange=null;renderRows();renderTimeline();return}if(e.button!==0)return;dragStart=timelinePoint(e);track.setPointerCapture?.(e.pointerId)};track.onpointerup=e=>{if(dragStart==null)return;const end=timelinePoint(e);timelineRange=[Math.min(dragStart,end),Math.max(dragStart,end)];dragStart=null;renderRows();renderTimeline()};track.oncontextmenu=e=>e.preventDefault();track.onkeydown=e=>{if(e.key==='Escape'){timelineRange=null;renderRows();renderTimeline()}};q('#turns').onclick=e=>{collapseTurns=!collapseTurns;e.currentTarget.classList.toggle('active',collapseTurns);e.currentTarget.setAttribute('aria-pressed',collapseTurns);renderRows()};q('#calls').onclick=e=>{collapseCalls=!collapseCalls;e.currentTarget.classList.toggle('active',collapseCalls);e.currentTarget.setAttribute('aria-pressed',collapseCalls);renderRows()};q('#duration').onclick=e=>{actualDuration=!actualDuration;e.currentTarget.classList.toggle('active',actualDuration);e.currentTarget.setAttribute('aria-pressed',actualDuration);timelineRange=null;renderRows();renderTimeline()};workspace.classList.add('details-closed');update();setInterval(update,
|
|
67
|
+
q('#detail-close').onclick=()=>{selected=null;details.hidden=true;workspace.classList.add('details-closed');renderRows();renderTimeline()};let searchTimer;q('#search').oninput=()=>{clearTimeout(searchTimer);searchTimer=setTimeout(renderRows,120)};
|
|
68
|
+
function timelinePoint(e){const d=timelineModel(),box=track.getBoundingClientRect();return d?d.min+Math.max(0,Math.min(1,(e.clientX-box.left)/box.width))*(d.max-d.min):0}track.onpointerdown=e=>{if(e.button===2){timelineRange=null;renderRows();renderTimeline();return}if(e.button!==0)return;dragStart=timelinePoint(e);track.setPointerCapture?.(e.pointerId)};track.onpointerup=e=>{if(dragStart==null)return;const end=timelinePoint(e);timelineRange=[Math.min(dragStart,end),Math.max(dragStart,end)];dragStart=null;renderRows();renderTimeline()};track.oncontextmenu=e=>e.preventDefault();track.onkeydown=e=>{if(e.key==='Escape'){timelineRange=null;renderRows();renderTimeline()}};q('#turns').onclick=e=>{collapseTurns=!collapseTurns;e.currentTarget.classList.toggle('active',collapseTurns);e.currentTarget.setAttribute('aria-pressed',collapseTurns);renderRows()};q('#calls').onclick=e=>{collapseCalls=!collapseCalls;e.currentTarget.classList.toggle('active',collapseCalls);e.currentTarget.setAttribute('aria-pressed',collapseCalls);renderRows()};q('#unipi-only').onclick=e=>{unipiOnly=!unipiOnly;e.currentTarget.classList.toggle('active',unipiOnly);e.currentTarget.setAttribute('aria-pressed',unipiOnly);renderRows()};q('#violations').onclick=e=>{violationsOnly=!violationsOnly;e.currentTarget.classList.toggle('active',violationsOnly);e.currentTarget.setAttribute('aria-pressed',violationsOnly);renderRows()};q('#duration').onclick=e=>{actualDuration=!actualDuration;e.currentTarget.classList.toggle('active',actualDuration);e.currentTarget.setAttribute('aria-pressed',actualDuration);timelineRange=null;renderRows();renderTimeline()};workspace.classList.add('details-closed');update();setInterval(update,1500);
|
|
67
69
|
`;
|