copilot-tap-extension 2.0.5 → 2.0.7
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/README.md +16 -3
- package/dist/copilot-instructions.md +20 -9
- package/dist/extension.mjs +1190 -31
- package/dist/skills/tap-goal/SKILL.md +116 -31
- package/dist/version.json +1 -1
- package/package.json +2 -2
package/dist/extension.mjs
CHANGED
|
@@ -2284,7 +2284,7 @@ var require_websocket = __commonJS({
|
|
|
2284
2284
|
var tls = __require("tls");
|
|
2285
2285
|
var { randomBytes: randomBytes3, createHash } = __require("crypto");
|
|
2286
2286
|
var { Duplex, Readable } = __require("stream");
|
|
2287
|
-
var { URL } = __require("url");
|
|
2287
|
+
var { URL: URL2 } = __require("url");
|
|
2288
2288
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
2289
2289
|
var Receiver2 = require_receiver();
|
|
2290
2290
|
var Sender2 = require_sender();
|
|
@@ -2785,11 +2785,11 @@ var require_websocket = __commonJS({
|
|
|
2785
2785
|
);
|
|
2786
2786
|
}
|
|
2787
2787
|
let parsedUrl;
|
|
2788
|
-
if (address instanceof
|
|
2788
|
+
if (address instanceof URL2) {
|
|
2789
2789
|
parsedUrl = address;
|
|
2790
2790
|
} else {
|
|
2791
2791
|
try {
|
|
2792
|
-
parsedUrl = new
|
|
2792
|
+
parsedUrl = new URL2(address);
|
|
2793
2793
|
} catch {
|
|
2794
2794
|
throw new SyntaxError(`Invalid URL: ${address}`);
|
|
2795
2795
|
}
|
|
@@ -2926,7 +2926,7 @@ var require_websocket = __commonJS({
|
|
|
2926
2926
|
req.abort();
|
|
2927
2927
|
let addr;
|
|
2928
2928
|
try {
|
|
2929
|
-
addr = new
|
|
2929
|
+
addr = new URL2(location, address);
|
|
2930
2930
|
} catch (e) {
|
|
2931
2931
|
const err = new SyntaxError(`Invalid URL: ${location}`);
|
|
2932
2932
|
emitErrorAndClose(websocket, err);
|
|
@@ -4568,12 +4568,12 @@ function createEmitterTools(deps) {
|
|
|
4568
4568
|
properties: {
|
|
4569
4569
|
name: { type: "string", description: "Unique emitter name." },
|
|
4570
4570
|
command: { type: "string", description: "Shell command to run (creates a CommandEmitter). Output goes through EventFilter rules to determine whether lines are kept, surfaced, or injected. Use for log tailing, process monitoring, or any external command with stdout." },
|
|
4571
|
-
prompt: { type: "string", description: "Prompt to send to the agent (creates a PromptEmitter). Always injects \u2014 bypasses EventFilter entirely. Use for repeated agent tasks, status checks, or
|
|
4571
|
+
prompt: { type: "string", description: "Prompt to send to the agent (creates a PromptEmitter). Always injects \u2014 bypasses EventFilter entirely. Use for repeated agent tasks, status checks, simple messages, or autonomous goal loops." },
|
|
4572
4572
|
description: { type: "string", description: "Short summary." },
|
|
4573
4573
|
channel: { type: "string", description: "EventStream to receive accepted events." },
|
|
4574
4574
|
cwd: { type: "string", description: "Optional subdirectory relative to the session cwd. Absolute paths and paths that escape the session cwd are rejected." },
|
|
4575
|
-
every: { type: "string", description: "Optional repeat interval like 30s, 5m, 2h, or 1d (maximum about 24 days). Use 'idle' for
|
|
4576
|
-
everySchedule: { type: "array", minItems: 1, items: { type: "string" }, description: "Optional backoff schedule \u2014 an ordered non-empty list of interval strings (e.g. ['10s','20s','30s','1m','2m','5m','10m']; each maximum about 24 days). The emitter uses each interval in sequence, then repeats the last one forever. Overrides 'every' when provided. Cannot be 'idle' entries." },
|
|
4575
|
+
every: { type: "string", description: "Optional repeat interval like 30s, 5m, 2h, or 1d (maximum about 24 days). Use 'idle' for conservative prompt loops that re-run only when the session is idle. When omitted, commands run continuously and prompts run once." },
|
|
4576
|
+
everySchedule: { type: "array", minItems: 1, items: { type: "string" }, description: "Optional backoff schedule \u2014 an ordered non-empty list of interval strings (e.g. ['10s','20s','30s','1m','2m','5m','10m']; each maximum about 24 days). The emitter uses each interval in sequence, then repeats the last one forever. Overrides 'every' when provided. Cannot be 'idle' entries. Prefer this for autopilot-compatible goal loops that may need timed nudges while the session stays busy." },
|
|
4577
4577
|
lifespan: policyLifespanParameter(),
|
|
4578
4578
|
ownership: policyOwnershipParameter(),
|
|
4579
4579
|
scope: policyScopeParameter(),
|
|
@@ -4583,7 +4583,7 @@ function createEmitterTools(deps) {
|
|
|
4583
4583
|
eventFilter: EVENT_FILTER_PARAMETER_SCHEMA,
|
|
4584
4584
|
subscribe: { type: "boolean", description: "Whether to attach a session injector to the stream as part of emitter creation." },
|
|
4585
4585
|
delivery: { type: "string", description: "Session injector delivery mode. 'important'/'inject' only send inject-outcome lines, 'surface' surfaces surface outcomes and sends inject outcomes, 'all' surfaces all accepted lines while inject outcomes still push into the conversation, and 'keep'/'drop' store without proactive delivery." },
|
|
4586
|
-
maxRuns: { type: "integer", description: "Maximum number of iterations before the emitter
|
|
4586
|
+
maxRuns: { type: "integer", description: "Maximum number of real iterations before the emitter stops. Useful for idle and timed loops. For goal loops this is a budget limit, not proof that the objective is complete." },
|
|
4587
4587
|
force: policyForceParameter("emitter")
|
|
4588
4588
|
},
|
|
4589
4589
|
required: ["name"]
|
|
@@ -4643,14 +4643,59 @@ function createEmitterTools(deps) {
|
|
|
4643
4643
|
];
|
|
4644
4644
|
}
|
|
4645
4645
|
|
|
4646
|
+
// src/tools/diagnostics.mjs
|
|
4647
|
+
function renderCanvasOpenResult(result) {
|
|
4648
|
+
return [
|
|
4649
|
+
"Opened tap diagnostics canvas.",
|
|
4650
|
+
result?.title ? `title=${result.title}` : null,
|
|
4651
|
+
result?.status ? `status=${result.status}` : null,
|
|
4652
|
+
result?.url ? `url=${result.url}` : null,
|
|
4653
|
+
result?.instanceId ? `instanceId=${result.instanceId}` : null,
|
|
4654
|
+
result?.availability ? `availability=${result.availability}` : null
|
|
4655
|
+
].filter(Boolean).join("\n");
|
|
4656
|
+
}
|
|
4657
|
+
function createDiagnosticsTools(deps) {
|
|
4658
|
+
const diagnostics2 = deps.diagnostics ?? deps.runtime;
|
|
4659
|
+
if (!diagnostics2 || typeof diagnostics2.openCanvas !== "function") {
|
|
4660
|
+
return [];
|
|
4661
|
+
}
|
|
4662
|
+
return [
|
|
4663
|
+
{
|
|
4664
|
+
name: "tap_open_diagnostics_canvas",
|
|
4665
|
+
description: "Opens or focuses the Tap diagnostics canvas, a live flight recorder for streams, emitters, providers, logs, injection queues, and session events.",
|
|
4666
|
+
parameters: {
|
|
4667
|
+
type: "object",
|
|
4668
|
+
properties: {
|
|
4669
|
+
instanceId: {
|
|
4670
|
+
type: "string",
|
|
4671
|
+
description: "Stable canvas instance id. Reusing the same id focuses the existing diagnostics canvas."
|
|
4672
|
+
},
|
|
4673
|
+
limit: {
|
|
4674
|
+
type: "integer",
|
|
4675
|
+
minimum: 10,
|
|
4676
|
+
maximum: 300,
|
|
4677
|
+
description: "Maximum retained rows to show per diagnostics section."
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
},
|
|
4681
|
+
handler: wrapToolHandler("tap_open_diagnostics_canvas", {}, async (args) => {
|
|
4682
|
+
const result = await diagnostics2.openCanvas(args ?? {});
|
|
4683
|
+
return renderCanvasOpenResult(result);
|
|
4684
|
+
})
|
|
4685
|
+
}
|
|
4686
|
+
];
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4646
4689
|
// src/tools/index.mjs
|
|
4647
4690
|
function createTools(deps) {
|
|
4648
4691
|
const source = deps?.tools ?? deps?.runtime?.tools ?? {};
|
|
4649
4692
|
const streams = deps?.streams ?? source.streams ?? deps?.runtime;
|
|
4650
4693
|
const emitters = deps?.emitters ?? source.emitters ?? deps?.runtime;
|
|
4694
|
+
const diagnostics2 = deps?.diagnostics ?? source.diagnostics ?? deps?.runtime;
|
|
4651
4695
|
return [
|
|
4652
4696
|
...createStreamTools({ streams }),
|
|
4653
|
-
...createEmitterTools({ emitters })
|
|
4697
|
+
...createEmitterTools({ emitters }),
|
|
4698
|
+
...createDiagnosticsTools({ diagnostics: diagnostics2 })
|
|
4654
4699
|
];
|
|
4655
4700
|
}
|
|
4656
4701
|
|
|
@@ -6199,6 +6244,9 @@ function createProviderConnection(ws, options, adapters = {}) {
|
|
|
6199
6244
|
get tools() {
|
|
6200
6245
|
return tools;
|
|
6201
6246
|
},
|
|
6247
|
+
get pendingCallCount() {
|
|
6248
|
+
return pendingCalls.size;
|
|
6249
|
+
},
|
|
6202
6250
|
sendToolCall: sendToolCallMsg,
|
|
6203
6251
|
sendToolCancel: sendToolCancelMsg,
|
|
6204
6252
|
sendLifecycle,
|
|
@@ -6300,6 +6348,20 @@ function createDefaultPathAdapter() {
|
|
|
6300
6348
|
join: (...parts) => path2.join(...parts)
|
|
6301
6349
|
};
|
|
6302
6350
|
}
|
|
6351
|
+
function formatGatewayBindMessage(err, { host, port }) {
|
|
6352
|
+
const message = String(err?.message ?? err ?? "unknown error");
|
|
6353
|
+
const code = String(err?.code ?? "");
|
|
6354
|
+
const isPortInUse = code === "EADDRINUSE" || /\bEADDRINUSE\b|address already in use/i.test(message);
|
|
6355
|
+
if (!isPortInUse) {
|
|
6356
|
+
return `Provider gateway could not start on ${host}:${port}: ${message}`;
|
|
6357
|
+
}
|
|
6358
|
+
return [
|
|
6359
|
+
`Provider gateway mesh already has an owner at ${host}:${port}; this tap session is joining without binding another listener.`,
|
|
6360
|
+
"Core tap tools remain available.",
|
|
6361
|
+
"External providers should keep using the existing gateway.",
|
|
6362
|
+
"No action is needed unless provider tools are missing."
|
|
6363
|
+
].join(" ");
|
|
6364
|
+
}
|
|
6303
6365
|
function createProviderGateway(options = {}, adapters = {}) {
|
|
6304
6366
|
const {
|
|
6305
6367
|
tapTools,
|
|
@@ -6592,7 +6654,7 @@ function createProviderGateway(options = {}, adapters = {}) {
|
|
|
6592
6654
|
}
|
|
6593
6655
|
}
|
|
6594
6656
|
function resetFailedStart(server, err) {
|
|
6595
|
-
log(
|
|
6657
|
+
log(formatGatewayBindMessage(err, { host, port: GATEWAY_PORT }));
|
|
6596
6658
|
starting = false;
|
|
6597
6659
|
if (wss === server) {
|
|
6598
6660
|
wss = null;
|
|
@@ -6660,6 +6722,41 @@ function createProviderGateway(options = {}, adapters = {}) {
|
|
|
6660
6722
|
const tap = currentTapTools || (typeof tapTools === "function" ? tapTools() : []);
|
|
6661
6723
|
return registry.buildSessionTools(tap, dispatchToolCall);
|
|
6662
6724
|
}
|
|
6725
|
+
function projectProviderTool(tool) {
|
|
6726
|
+
return {
|
|
6727
|
+
name: tool.name,
|
|
6728
|
+
description: tool.description ?? "",
|
|
6729
|
+
timeout: tool.timeout ?? null
|
|
6730
|
+
};
|
|
6731
|
+
}
|
|
6732
|
+
function getDiagnosticState() {
|
|
6733
|
+
const connections = [...connectionsByProviderId.values()];
|
|
6734
|
+
return {
|
|
6735
|
+
running,
|
|
6736
|
+
starting,
|
|
6737
|
+
host,
|
|
6738
|
+
port: GATEWAY_PORT,
|
|
6739
|
+
reloadPending,
|
|
6740
|
+
reloadTimerActive: Boolean(reloadTimer),
|
|
6741
|
+
tokenPresent: Boolean(token),
|
|
6742
|
+
providerTokenFilePath: providerTokenFilePath ? "[redacted]" : null,
|
|
6743
|
+
gracefulShutdownActive,
|
|
6744
|
+
connectionCount: connectionsByWs.size,
|
|
6745
|
+
boundConnectionCount: connections.filter((conn) => conn.state === CONNECTION_STATE.BOUND).length,
|
|
6746
|
+
providers: registry.listProviders().map((provider) => {
|
|
6747
|
+
const conn = connectionsByProviderId.get(provider.id);
|
|
6748
|
+
return {
|
|
6749
|
+
id: provider.id,
|
|
6750
|
+
name: provider.name,
|
|
6751
|
+
sessionId: provider.sessionId,
|
|
6752
|
+
state: conn?.state ?? "unknown",
|
|
6753
|
+
pendingCallCount: conn?.pendingCallCount ?? 0,
|
|
6754
|
+
toolCount: provider.tools.length,
|
|
6755
|
+
tools: provider.tools.map(projectProviderTool)
|
|
6756
|
+
};
|
|
6757
|
+
})
|
|
6758
|
+
};
|
|
6759
|
+
}
|
|
6663
6760
|
function dispatchToolCall(providerId, toolName, callId, args) {
|
|
6664
6761
|
const conn = connectionsByProviderId.get(providerId);
|
|
6665
6762
|
if (!conn || conn.state === CONNECTION_STATE.DISCONNECTED) {
|
|
@@ -6696,6 +6793,7 @@ function createProviderGateway(options = {}, adapters = {}) {
|
|
|
6696
6793
|
getToken,
|
|
6697
6794
|
getRegistry,
|
|
6698
6795
|
getAllTools,
|
|
6796
|
+
getDiagnosticState,
|
|
6699
6797
|
dispatchToolCall,
|
|
6700
6798
|
broadcastLifecycle,
|
|
6701
6799
|
onToolsChanged,
|
|
@@ -8905,7 +9003,7 @@ function buildCompleteMessage(state) {
|
|
|
8905
9003
|
return `Emitter '${state.name}' completed one run of ${state.emitterType} work.`;
|
|
8906
9004
|
}
|
|
8907
9005
|
if (state.maxRuns && state.runCount >= state.maxRuns) {
|
|
8908
|
-
return `Emitter '${state.name}'
|
|
9006
|
+
return `Emitter '${state.name}' reached its run budget (${state.runCount} of ${state.maxRuns} runs). This stops the emitter; goal-style loops must use their final evidence summary to decide whether the objective is complete.`;
|
|
8909
9007
|
}
|
|
8910
9008
|
return null;
|
|
8911
9009
|
}
|
|
@@ -9060,12 +9158,12 @@ function computeDeferredIterationTransition(currentState, state) {
|
|
|
9060
9158
|
};
|
|
9061
9159
|
}
|
|
9062
9160
|
function computeFailedIterationTransition(currentState, state, event, result) {
|
|
9063
|
-
if (isRunBudgetExhausted(state)) {
|
|
9064
|
-
return computeRunBudgetExhaustedTransition(currentState, state, event, result);
|
|
9065
|
-
}
|
|
9066
9161
|
if (result.deferred) {
|
|
9067
9162
|
return computeDeferredIterationTransition(currentState, state);
|
|
9068
9163
|
}
|
|
9164
|
+
if (isRunBudgetExhausted(state)) {
|
|
9165
|
+
return computeRunBudgetExhaustedTransition(currentState, state, event, result);
|
|
9166
|
+
}
|
|
9069
9167
|
if (state.runSchedule === RUN_SCHEDULE.ONE_TIME) {
|
|
9070
9168
|
return {
|
|
9071
9169
|
currentState,
|
|
@@ -9405,11 +9503,12 @@ async function runPromptIteration(emitter, context) {
|
|
|
9405
9503
|
} catch (error) {
|
|
9406
9504
|
const message = error?.message ?? String(error ?? "unknown error");
|
|
9407
9505
|
const sessionNotAttached = isSessionNotAttachedMessage(message);
|
|
9506
|
+
const deferred = sessionNotAttached || (emitter.runSchedule === RUN_SCHEDULE.TIMED || emitter.runSchedule === RUN_SCHEDULE.IDLE) && /\bsession\.idle\b/i.test(message);
|
|
9408
9507
|
return {
|
|
9409
9508
|
ok: false,
|
|
9410
9509
|
error: message,
|
|
9411
|
-
deferred
|
|
9412
|
-
consumeRun:
|
|
9510
|
+
deferred,
|
|
9511
|
+
consumeRun: deferred ? false : true,
|
|
9413
9512
|
deferredReason: sessionNotAttached ? "session-not-attached" : null
|
|
9414
9513
|
};
|
|
9415
9514
|
}
|
|
@@ -10258,6 +10357,16 @@ function createSessionPort(initialSession = null) {
|
|
|
10258
10357
|
}
|
|
10259
10358
|
return session2.sendAndWait({ prompt });
|
|
10260
10359
|
}
|
|
10360
|
+
async function openCanvas(params) {
|
|
10361
|
+
if (!session2) {
|
|
10362
|
+
throw new LifecycleError("Session is not attached; cannot open canvas.");
|
|
10363
|
+
}
|
|
10364
|
+
const canvasApi = session2.rpc?.canvas;
|
|
10365
|
+
if (!canvasApi || typeof canvasApi.open !== "function") {
|
|
10366
|
+
throw new LifecycleError("Canvas renderer API is not available in this Copilot session.");
|
|
10367
|
+
}
|
|
10368
|
+
return canvasApi.open(params);
|
|
10369
|
+
}
|
|
10261
10370
|
function registerTools(tools) {
|
|
10262
10371
|
if (!session2) return;
|
|
10263
10372
|
try {
|
|
@@ -10293,6 +10402,7 @@ function createSessionPort(initialSession = null) {
|
|
|
10293
10402
|
log,
|
|
10294
10403
|
send,
|
|
10295
10404
|
sendAndWait,
|
|
10405
|
+
openCanvas,
|
|
10296
10406
|
registerTools,
|
|
10297
10407
|
reloadExtension
|
|
10298
10408
|
};
|
|
@@ -10536,7 +10646,23 @@ function createNotificationDispatcher({
|
|
|
10536
10646
|
}
|
|
10537
10647
|
return { cleared, generation };
|
|
10538
10648
|
}
|
|
10539
|
-
|
|
10649
|
+
function snapshot(options = {}) {
|
|
10650
|
+
const limit = Math.max(0, Math.min(Number(options.limit ?? 20) || 20, queueLimit));
|
|
10651
|
+
return {
|
|
10652
|
+
queueSize: queue.length,
|
|
10653
|
+
maxQueueSize: queueLimit,
|
|
10654
|
+
inFlight,
|
|
10655
|
+
retryScheduled: retryTimer !== null,
|
|
10656
|
+
generation,
|
|
10657
|
+
queued: queue.slice(0, limit).map((entry) => ({
|
|
10658
|
+
channel: entry.channel,
|
|
10659
|
+
monitorName: entry.monitorName,
|
|
10660
|
+
stream: entry.stream,
|
|
10661
|
+
text: String(entry.text ?? "").slice(0, 500)
|
|
10662
|
+
}))
|
|
10663
|
+
};
|
|
10664
|
+
}
|
|
10665
|
+
return { enqueue, clear, dispose: clear, snapshot };
|
|
10540
10666
|
}
|
|
10541
10667
|
|
|
10542
10668
|
// src/services/runtime-subsystems.mjs
|
|
@@ -10732,8 +10858,263 @@ function createStreamService(deps) {
|
|
|
10732
10858
|
};
|
|
10733
10859
|
}
|
|
10734
10860
|
|
|
10861
|
+
// src/diagnostics/store.mjs
|
|
10862
|
+
var DEFAULT_MAX_LOGS = 300;
|
|
10863
|
+
var DEFAULT_MAX_EVENTS = 300;
|
|
10864
|
+
var DEFAULT_MAX_RUNTIME_EVENTS = 300;
|
|
10865
|
+
var MAX_STRING_LENGTH = 1200;
|
|
10866
|
+
var MAX_COLLECTION_ITEMS = 40;
|
|
10867
|
+
var MAX_DEPTH = 4;
|
|
10868
|
+
var SECRET_KEY_PATTERN = /(?:token|secret|password|credential|authorization|api[-_]?key|reconnectToken|expectedToken)/i;
|
|
10869
|
+
function normalizePositiveInteger(value, fallback) {
|
|
10870
|
+
const number = Number(value);
|
|
10871
|
+
if (!Number.isFinite(number)) {
|
|
10872
|
+
return fallback;
|
|
10873
|
+
}
|
|
10874
|
+
return Math.max(0, Math.floor(number));
|
|
10875
|
+
}
|
|
10876
|
+
function createRingBuffer(maxEntries) {
|
|
10877
|
+
const entries = [];
|
|
10878
|
+
const limit = normalizePositiveInteger(maxEntries, 0);
|
|
10879
|
+
let total = 0;
|
|
10880
|
+
let dropped = 0;
|
|
10881
|
+
function append(entry) {
|
|
10882
|
+
total += 1;
|
|
10883
|
+
if (limit === 0) {
|
|
10884
|
+
dropped += 1;
|
|
10885
|
+
return null;
|
|
10886
|
+
}
|
|
10887
|
+
entries.push(entry);
|
|
10888
|
+
if (entries.length > limit) {
|
|
10889
|
+
const overflow = entries.length - limit;
|
|
10890
|
+
entries.splice(0, overflow);
|
|
10891
|
+
dropped += overflow;
|
|
10892
|
+
}
|
|
10893
|
+
return entry;
|
|
10894
|
+
}
|
|
10895
|
+
function snapshot(limitOverride) {
|
|
10896
|
+
const requested = normalizePositiveInteger(limitOverride, entries.length);
|
|
10897
|
+
return entries.slice(Math.max(0, entries.length - requested)).map((entry) => safeClone(entry));
|
|
10898
|
+
}
|
|
10899
|
+
function stats() {
|
|
10900
|
+
return {
|
|
10901
|
+
retained: entries.length,
|
|
10902
|
+
total,
|
|
10903
|
+
dropped,
|
|
10904
|
+
capacity: limit
|
|
10905
|
+
};
|
|
10906
|
+
}
|
|
10907
|
+
return { append, snapshot, stats };
|
|
10908
|
+
}
|
|
10909
|
+
function truncateString(value, maxLength = MAX_STRING_LENGTH) {
|
|
10910
|
+
const text = String(value ?? "");
|
|
10911
|
+
if (text.length <= maxLength) {
|
|
10912
|
+
return text;
|
|
10913
|
+
}
|
|
10914
|
+
return `${text.slice(0, maxLength)}... (${text.length - maxLength} chars truncated)`;
|
|
10915
|
+
}
|
|
10916
|
+
function safeClone(value, options = {}, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
10917
|
+
const maxDepth = normalizePositiveInteger(options.maxDepth, MAX_DEPTH);
|
|
10918
|
+
const maxStringLength = normalizePositiveInteger(options.maxStringLength, MAX_STRING_LENGTH);
|
|
10919
|
+
const maxCollectionItems = normalizePositiveInteger(options.maxCollectionItems, MAX_COLLECTION_ITEMS);
|
|
10920
|
+
if (value === null || value === void 0) {
|
|
10921
|
+
return value ?? null;
|
|
10922
|
+
}
|
|
10923
|
+
if (typeof value === "string") {
|
|
10924
|
+
return truncateString(value, maxStringLength);
|
|
10925
|
+
}
|
|
10926
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
10927
|
+
return value;
|
|
10928
|
+
}
|
|
10929
|
+
if (typeof value === "bigint") {
|
|
10930
|
+
return String(value);
|
|
10931
|
+
}
|
|
10932
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
10933
|
+
return `[${typeof value}]`;
|
|
10934
|
+
}
|
|
10935
|
+
if (value instanceof Error) {
|
|
10936
|
+
return {
|
|
10937
|
+
name: value.name,
|
|
10938
|
+
message: truncateString(value.message, maxStringLength),
|
|
10939
|
+
stack: truncateString(value.stack ?? "", maxStringLength)
|
|
10940
|
+
};
|
|
10941
|
+
}
|
|
10942
|
+
if (depth >= maxDepth) {
|
|
10943
|
+
return Array.isArray(value) ? `[Array(${value.length})]` : "[Object]";
|
|
10944
|
+
}
|
|
10945
|
+
if (typeof value !== "object") {
|
|
10946
|
+
return String(value);
|
|
10947
|
+
}
|
|
10948
|
+
if (seen.has(value)) {
|
|
10949
|
+
return "[Circular]";
|
|
10950
|
+
}
|
|
10951
|
+
seen.add(value);
|
|
10952
|
+
if (Array.isArray(value)) {
|
|
10953
|
+
const slice = value.slice(0, maxCollectionItems).map((item) => safeClone(item, options, depth + 1, seen));
|
|
10954
|
+
if (value.length > maxCollectionItems) {
|
|
10955
|
+
slice.push(`... ${value.length - maxCollectionItems} more items`);
|
|
10956
|
+
}
|
|
10957
|
+
return slice;
|
|
10958
|
+
}
|
|
10959
|
+
const output = {};
|
|
10960
|
+
const entries = Object.entries(value).slice(0, maxCollectionItems);
|
|
10961
|
+
for (const [key, item] of entries) {
|
|
10962
|
+
output[key] = SECRET_KEY_PATTERN.test(key) ? "[redacted]" : safeClone(item, options, depth + 1, seen);
|
|
10963
|
+
}
|
|
10964
|
+
const remaining = Object.keys(value).length - entries.length;
|
|
10965
|
+
if (remaining > 0) {
|
|
10966
|
+
output.__truncatedKeys = remaining;
|
|
10967
|
+
}
|
|
10968
|
+
return output;
|
|
10969
|
+
}
|
|
10970
|
+
function normalizeLevel(value) {
|
|
10971
|
+
const level = String(value ?? "info").trim().toLowerCase();
|
|
10972
|
+
if (level === "warning" || level === "warn") return "warning";
|
|
10973
|
+
if (level === "error") return "error";
|
|
10974
|
+
if (level === "debug") return "debug";
|
|
10975
|
+
return "info";
|
|
10976
|
+
}
|
|
10977
|
+
function createId(prefix, count) {
|
|
10978
|
+
return `${prefix}-${count.toString(36)}`;
|
|
10979
|
+
}
|
|
10980
|
+
function summarizeSessionEvent(event) {
|
|
10981
|
+
const data = event?.data && typeof event.data === "object" ? event.data : {};
|
|
10982
|
+
return {
|
|
10983
|
+
id: event?.id ?? null,
|
|
10984
|
+
timestamp: event?.timestamp ?? nowIso(),
|
|
10985
|
+
type: String(event?.type ?? "unknown"),
|
|
10986
|
+
ephemeral: event?.ephemeral === true,
|
|
10987
|
+
agentId: event?.agentId ?? null,
|
|
10988
|
+
dataKeys: Object.keys(data),
|
|
10989
|
+
data: safeClone(data, {
|
|
10990
|
+
maxDepth: 3,
|
|
10991
|
+
maxStringLength: 700,
|
|
10992
|
+
maxCollectionItems: 24
|
|
10993
|
+
})
|
|
10994
|
+
};
|
|
10995
|
+
}
|
|
10996
|
+
function createDiagnosticsStore(options = {}) {
|
|
10997
|
+
const logs = createRingBuffer(options.maxLogs ?? DEFAULT_MAX_LOGS);
|
|
10998
|
+
const sessionEvents = createRingBuffer(options.maxSessionEvents ?? DEFAULT_MAX_EVENTS);
|
|
10999
|
+
const runtimeEvents = createRingBuffer(options.maxRuntimeEvents ?? DEFAULT_MAX_RUNTIME_EVENTS);
|
|
11000
|
+
const sessionEventCounts = /* @__PURE__ */ new Map();
|
|
11001
|
+
let logCount = 0;
|
|
11002
|
+
let runtimeEventCount = 0;
|
|
11003
|
+
let sessionEventCount = 0;
|
|
11004
|
+
let cleanupSessionListener = () => {
|
|
11005
|
+
};
|
|
11006
|
+
function recordLog(source, message, options2 = {}) {
|
|
11007
|
+
logCount += 1;
|
|
11008
|
+
return logs.append({
|
|
11009
|
+
id: createId("log", logCount),
|
|
11010
|
+
timestamp: nowIso(),
|
|
11011
|
+
source: String(source ?? "tap"),
|
|
11012
|
+
level: normalizeLevel(options2.level),
|
|
11013
|
+
message: truncateString(message, options2.maxStringLength ?? MAX_STRING_LENGTH),
|
|
11014
|
+
metadata: safeClone(options2.metadata ?? null, {
|
|
11015
|
+
maxDepth: 3,
|
|
11016
|
+
maxStringLength: 700,
|
|
11017
|
+
maxCollectionItems: 20
|
|
11018
|
+
})
|
|
11019
|
+
});
|
|
11020
|
+
}
|
|
11021
|
+
function recordRuntimeEvent(type, message, metadata = {}) {
|
|
11022
|
+
runtimeEventCount += 1;
|
|
11023
|
+
return runtimeEvents.append({
|
|
11024
|
+
id: createId("evt", runtimeEventCount),
|
|
11025
|
+
timestamp: nowIso(),
|
|
11026
|
+
type: String(type ?? "runtime"),
|
|
11027
|
+
message: truncateString(message, MAX_STRING_LENGTH),
|
|
11028
|
+
metadata: safeClone(metadata, {
|
|
11029
|
+
maxDepth: 3,
|
|
11030
|
+
maxStringLength: 700,
|
|
11031
|
+
maxCollectionItems: 20
|
|
11032
|
+
})
|
|
11033
|
+
});
|
|
11034
|
+
}
|
|
11035
|
+
function recordSessionEvent(event) {
|
|
11036
|
+
sessionEventCount += 1;
|
|
11037
|
+
const type = String(event?.type ?? "unknown");
|
|
11038
|
+
sessionEventCounts.set(type, (sessionEventCounts.get(type) ?? 0) + 1);
|
|
11039
|
+
return sessionEvents.append({
|
|
11040
|
+
sequence: sessionEventCount,
|
|
11041
|
+
...summarizeSessionEvent(event)
|
|
11042
|
+
});
|
|
11043
|
+
}
|
|
11044
|
+
function detachSession() {
|
|
11045
|
+
try {
|
|
11046
|
+
cleanupSessionListener();
|
|
11047
|
+
} catch {
|
|
11048
|
+
}
|
|
11049
|
+
cleanupSessionListener = () => {
|
|
11050
|
+
};
|
|
11051
|
+
}
|
|
11052
|
+
function attachSession(session2) {
|
|
11053
|
+
detachSession();
|
|
11054
|
+
if (!session2 || typeof session2.on !== "function") {
|
|
11055
|
+
recordRuntimeEvent("session-events", "Session event capture unavailable; no session listener attached.");
|
|
11056
|
+
return;
|
|
11057
|
+
}
|
|
11058
|
+
try {
|
|
11059
|
+
const unsubscribe = session2.on((event) => {
|
|
11060
|
+
recordSessionEvent(event);
|
|
11061
|
+
});
|
|
11062
|
+
cleanupSessionListener = typeof unsubscribe === "function" ? unsubscribe : () => {
|
|
11063
|
+
};
|
|
11064
|
+
recordRuntimeEvent("session-events", "Session event capture attached.");
|
|
11065
|
+
} catch (error) {
|
|
11066
|
+
recordLog("diagnostics", "Failed to attach session event capture.", {
|
|
11067
|
+
level: "warning",
|
|
11068
|
+
metadata: { error }
|
|
11069
|
+
});
|
|
11070
|
+
}
|
|
11071
|
+
}
|
|
11072
|
+
function snapshot(options2 = {}) {
|
|
11073
|
+
return {
|
|
11074
|
+
generatedAt: nowIso(),
|
|
11075
|
+
logs: logs.snapshot(options2.logLimit ?? 140),
|
|
11076
|
+
runtimeEvents: runtimeEvents.snapshot(options2.runtimeEventLimit ?? 140),
|
|
11077
|
+
sessionEvents: sessionEvents.snapshot(options2.sessionEventLimit ?? 140),
|
|
11078
|
+
sessionEventCounts: Object.fromEntries([...sessionEventCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
|
|
11079
|
+
stats: {
|
|
11080
|
+
logs: logs.stats(),
|
|
11081
|
+
runtimeEvents: runtimeEvents.stats(),
|
|
11082
|
+
sessionEvents: sessionEvents.stats()
|
|
11083
|
+
}
|
|
11084
|
+
};
|
|
11085
|
+
}
|
|
11086
|
+
return {
|
|
11087
|
+
log: recordLog,
|
|
11088
|
+
event: recordRuntimeEvent,
|
|
11089
|
+
attachSession,
|
|
11090
|
+
detachSession,
|
|
11091
|
+
snapshot
|
|
11092
|
+
};
|
|
11093
|
+
}
|
|
11094
|
+
|
|
11095
|
+
// src/canvas/consts.mjs
|
|
11096
|
+
var TAP_DIAGNOSTICS_CANVAS_ID = "tap-diagnostics";
|
|
11097
|
+
|
|
10735
11098
|
// src/services/tap-runtime-service.mjs
|
|
11099
|
+
function trimStreamEntries(stream, limit) {
|
|
11100
|
+
const entryLimit = Math.max(0, Math.floor(Number(limit ?? 80) || 80));
|
|
11101
|
+
return {
|
|
11102
|
+
...stream,
|
|
11103
|
+
entries: Array.isArray(stream.entries) ? stream.entries.slice(Math.max(0, stream.entries.length - entryLimit)) : []
|
|
11104
|
+
};
|
|
11105
|
+
}
|
|
11106
|
+
function projectToolForDiagnostics(tool) {
|
|
11107
|
+
return {
|
|
11108
|
+
name: tool.name,
|
|
11109
|
+
description: tool.description ?? "",
|
|
11110
|
+
providerId: tool.providerId ?? null,
|
|
11111
|
+
providerName: tool.providerName ?? null,
|
|
11112
|
+
skipPermission: tool.skipPermission === true,
|
|
11113
|
+
overridesBuiltInTool: tool.overridesBuiltInTool === true
|
|
11114
|
+
};
|
|
11115
|
+
}
|
|
10736
11116
|
function createTapRuntimeService(options = {}) {
|
|
11117
|
+
const diagnosticsStore = options.diagnostics ?? createDiagnosticsStore();
|
|
10737
11118
|
const {
|
|
10738
11119
|
streams,
|
|
10739
11120
|
configStore,
|
|
@@ -10780,6 +11161,7 @@ function createTapRuntimeService(options = {}) {
|
|
|
10780
11161
|
sessionContext.attachSession(session2);
|
|
10781
11162
|
sessionPort.attach(session2);
|
|
10782
11163
|
sessionActivityBridge.attach(session2);
|
|
11164
|
+
diagnosticsStore.attachSession(session2);
|
|
10783
11165
|
}
|
|
10784
11166
|
function clearNotificationsForLifecycle(options2 = {}) {
|
|
10785
11167
|
if (options2.clearNotifications === true && typeof notifications.clear === "function") {
|
|
@@ -10852,6 +11234,7 @@ function createTapRuntimeService(options = {}) {
|
|
|
10852
11234
|
const providerCapabilities = {
|
|
10853
11235
|
getSessionInfo,
|
|
10854
11236
|
log: (msg) => {
|
|
11237
|
+
diagnosticsStore.log("gateway", msg);
|
|
10855
11238
|
process.stderr.write(`[tap-gateway] ${msg}
|
|
10856
11239
|
`);
|
|
10857
11240
|
void sessionPort.log(msg);
|
|
@@ -10862,14 +11245,53 @@ function createTapRuntimeService(options = {}) {
|
|
|
10862
11245
|
void sessionPort.reloadExtension();
|
|
10863
11246
|
}
|
|
10864
11247
|
};
|
|
11248
|
+
function getDiagnosticsSnapshot(options2 = {}, extra = {}) {
|
|
11249
|
+
const allTools = Array.isArray(extra.tools) ? extra.tools.map(projectToolForDiagnostics) : [];
|
|
11250
|
+
return {
|
|
11251
|
+
generatedAt: nowIso(),
|
|
11252
|
+
session: getSessionInfo(),
|
|
11253
|
+
baseCwd: sessionContext.getBaseCwd(),
|
|
11254
|
+
process: {
|
|
11255
|
+
pid: process.pid,
|
|
11256
|
+
platform: process.platform,
|
|
11257
|
+
uptimeSeconds: Math.round(process.uptime())
|
|
11258
|
+
},
|
|
11259
|
+
streams: streamCapabilities.listStreams().map((stream) => trimStreamEntries(stream, options2.streamEntryLimit)),
|
|
11260
|
+
emitters: emitterCapabilities.listEmitters(),
|
|
11261
|
+
gateway: extra.gateway ?? null,
|
|
11262
|
+
tools: allTools,
|
|
11263
|
+
notifications: typeof notifications.snapshot === "function" ? notifications.snapshot({ limit: options2.notificationLimit ?? 20 }) : null,
|
|
11264
|
+
diagnostics: diagnosticsStore.snapshot(options2)
|
|
11265
|
+
};
|
|
11266
|
+
}
|
|
11267
|
+
const diagnosticsCapabilities = {
|
|
11268
|
+
log: (source, message, options2 = {}) => diagnosticsStore.log(source, message, options2),
|
|
11269
|
+
event: (type, message, metadata = {}) => diagnosticsStore.event(type, message, metadata),
|
|
11270
|
+
snapshot: getDiagnosticsSnapshot,
|
|
11271
|
+
openCanvas: async (input = {}) => {
|
|
11272
|
+
const instanceId = String(input.instanceId ?? TAP_DIAGNOSTICS_CANVAS_ID).trim() || TAP_DIAGNOSTICS_CANVAS_ID;
|
|
11273
|
+
const limit = Number(input.limit);
|
|
11274
|
+
const canvasInput = Number.isFinite(limit) ? { limit: Math.max(10, Math.min(300, Math.floor(limit))) } : {};
|
|
11275
|
+
diagnosticsStore.event("canvas.open.requested", "Opening tap diagnostics canvas.", { instanceId, canvasInput });
|
|
11276
|
+
return sessionPort.openCanvas({
|
|
11277
|
+
canvasId: TAP_DIAGNOSTICS_CANVAS_ID,
|
|
11278
|
+
instanceId,
|
|
11279
|
+
input: canvasInput
|
|
11280
|
+
});
|
|
11281
|
+
},
|
|
11282
|
+
attachSession: diagnosticsStore.attachSession,
|
|
11283
|
+
detachSession: diagnosticsStore.detachSession
|
|
11284
|
+
};
|
|
10865
11285
|
return {
|
|
10866
11286
|
tools: {
|
|
10867
11287
|
streams: streamCapabilities,
|
|
10868
|
-
emitters: emitterCapabilities
|
|
11288
|
+
emitters: emitterCapabilities,
|
|
11289
|
+
diagnostics: diagnosticsCapabilities
|
|
10869
11290
|
},
|
|
10870
11291
|
hooks: hookCapabilities,
|
|
10871
11292
|
session: sessionCapabilities,
|
|
10872
11293
|
provider: providerCapabilities,
|
|
11294
|
+
diagnostics: diagnosticsCapabilities,
|
|
10873
11295
|
getBaseCwd: sessionContext.getBaseCwd,
|
|
10874
11296
|
getSessionInfo,
|
|
10875
11297
|
attachSession,
|
|
@@ -10892,6 +11314,725 @@ function createTapRuntimeService(options = {}) {
|
|
|
10892
11314
|
};
|
|
10893
11315
|
}
|
|
10894
11316
|
|
|
11317
|
+
// src/canvas/diagnostics-canvas.mjs
|
|
11318
|
+
import { createServer } from "node:http";
|
|
11319
|
+
import { createCanvas } from "@github/copilot-sdk/extension";
|
|
11320
|
+
var DEFAULT_REFRESH_MS = 1200;
|
|
11321
|
+
function jsonResponse(res, status, data) {
|
|
11322
|
+
const body = JSON.stringify(data, null, 2);
|
|
11323
|
+
res.writeHead(status, {
|
|
11324
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
11325
|
+
"Cache-Control": "no-store",
|
|
11326
|
+
"X-Content-Type-Options": "nosniff"
|
|
11327
|
+
});
|
|
11328
|
+
res.end(body);
|
|
11329
|
+
}
|
|
11330
|
+
function textResponse(res, status, body, contentType = "text/plain; charset=utf-8") {
|
|
11331
|
+
res.writeHead(status, {
|
|
11332
|
+
"Content-Type": contentType,
|
|
11333
|
+
"Cache-Control": "no-store",
|
|
11334
|
+
"X-Content-Type-Options": "nosniff"
|
|
11335
|
+
});
|
|
11336
|
+
res.end(body);
|
|
11337
|
+
}
|
|
11338
|
+
function sanitizeSnapshotOptions(input = {}) {
|
|
11339
|
+
const source = input && typeof input === "object" ? input : {};
|
|
11340
|
+
const limit = Number(source.limit);
|
|
11341
|
+
return {
|
|
11342
|
+
streamEntryLimit: Number.isFinite(limit) ? Math.max(10, Math.min(200, Math.floor(limit))) : 80,
|
|
11343
|
+
logLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
|
|
11344
|
+
sessionEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
|
|
11345
|
+
runtimeEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160
|
|
11346
|
+
};
|
|
11347
|
+
}
|
|
11348
|
+
function summarizeSnapshot(snapshot) {
|
|
11349
|
+
const runningEmitters = Array.isArray(snapshot?.emitters?.running) ? snapshot.emitters.running.length : 0;
|
|
11350
|
+
const configuredEmitters = Array.isArray(snapshot?.emitters?.configured) ? snapshot.emitters.configured.length : 0;
|
|
11351
|
+
const streams = Array.isArray(snapshot?.streams) ? snapshot.streams.length : 0;
|
|
11352
|
+
const providers = Array.isArray(snapshot?.gateway?.providers) ? snapshot.gateway.providers.length : 0;
|
|
11353
|
+
const logs = snapshot?.diagnostics?.stats?.logs?.retained ?? 0;
|
|
11354
|
+
const sessionEvents = snapshot?.diagnostics?.stats?.sessionEvents?.retained ?? 0;
|
|
11355
|
+
return { streams, runningEmitters, configuredEmitters, providers, logs, sessionEvents };
|
|
11356
|
+
}
|
|
11357
|
+
function createHtml() {
|
|
11358
|
+
return `<!doctype html>
|
|
11359
|
+
<html lang="en">
|
|
11360
|
+
<head>
|
|
11361
|
+
<meta charset="utf-8" />
|
|
11362
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
11363
|
+
<title>tap diagnostics</title>
|
|
11364
|
+
<style>
|
|
11365
|
+
:root {
|
|
11366
|
+
color-scheme: light;
|
|
11367
|
+
--paper: oklch(96% 0.018 86);
|
|
11368
|
+
--paper-2: oklch(91% 0.025 82);
|
|
11369
|
+
--ink: oklch(18% 0.026 70);
|
|
11370
|
+
--muted: oklch(43% 0.04 73);
|
|
11371
|
+
--line: oklch(72% 0.052 76);
|
|
11372
|
+
--amber: oklch(70% 0.15 72);
|
|
11373
|
+
--amber-dark: oklch(42% 0.12 67);
|
|
11374
|
+
--red: oklch(55% 0.18 30);
|
|
11375
|
+
--green: oklch(55% 0.13 145);
|
|
11376
|
+
--blue: oklch(48% 0.1 245);
|
|
11377
|
+
--surface: oklch(99% 0.012 88);
|
|
11378
|
+
--shadow: 0 24px 80px oklch(28% 0.04 70 / 0.14);
|
|
11379
|
+
--font-display: "Bahnschrift", "Aptos Display", "Segoe UI Variable Display", sans-serif;
|
|
11380
|
+
--font-body: "Aptos", "Segoe UI Variable Text", sans-serif;
|
|
11381
|
+
--font-mono: "Cascadia Code", "Consolas", monospace;
|
|
11382
|
+
}
|
|
11383
|
+
|
|
11384
|
+
* { box-sizing: border-box; }
|
|
11385
|
+
html { min-height: 100%; background: var(--paper); color: var(--ink); }
|
|
11386
|
+
body {
|
|
11387
|
+
margin: 0;
|
|
11388
|
+
min-height: 100vh;
|
|
11389
|
+
font-family: var(--font-body);
|
|
11390
|
+
background:
|
|
11391
|
+
linear-gradient(90deg, oklch(34% 0.08 68 / 0.08) 1px, transparent 1px) 0 0 / 42px 42px,
|
|
11392
|
+
linear-gradient(0deg, oklch(34% 0.08 68 / 0.055) 1px, transparent 1px) 0 0 / 42px 42px,
|
|
11393
|
+
radial-gradient(circle at 8% 12%, oklch(76% 0.13 74 / 0.35), transparent 28rem),
|
|
11394
|
+
var(--paper);
|
|
11395
|
+
}
|
|
11396
|
+
|
|
11397
|
+
button, input { font: inherit; }
|
|
11398
|
+
button:focus-visible, input:focus-visible {
|
|
11399
|
+
outline: 3px solid var(--amber-dark);
|
|
11400
|
+
outline-offset: 3px;
|
|
11401
|
+
}
|
|
11402
|
+
|
|
11403
|
+
.shell {
|
|
11404
|
+
width: min(1540px, calc(100vw - clamp(18px, 4vw, 64px)));
|
|
11405
|
+
margin: 0 auto;
|
|
11406
|
+
padding: clamp(22px, 4vw, 54px) 0;
|
|
11407
|
+
}
|
|
11408
|
+
|
|
11409
|
+
.masthead {
|
|
11410
|
+
display: grid;
|
|
11411
|
+
grid-template-columns: minmax(0, 1fr);
|
|
11412
|
+
gap: 22px;
|
|
11413
|
+
align-items: end;
|
|
11414
|
+
border-bottom: 3px solid var(--ink);
|
|
11415
|
+
padding-bottom: clamp(18px, 3vw, 34px);
|
|
11416
|
+
}
|
|
11417
|
+
|
|
11418
|
+
.mark {
|
|
11419
|
+
width: 72px;
|
|
11420
|
+
aspect-ratio: 1;
|
|
11421
|
+
display: grid;
|
|
11422
|
+
place-items: center;
|
|
11423
|
+
border: 3px solid var(--ink);
|
|
11424
|
+
background: var(--amber);
|
|
11425
|
+
box-shadow: 8px 8px 0 var(--ink);
|
|
11426
|
+
font-family: var(--font-display);
|
|
11427
|
+
font-size: 44px;
|
|
11428
|
+
line-height: 1;
|
|
11429
|
+
}
|
|
11430
|
+
|
|
11431
|
+
.title-wrap {
|
|
11432
|
+
display: grid;
|
|
11433
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
11434
|
+
gap: clamp(16px, 3vw, 28px);
|
|
11435
|
+
align-items: center;
|
|
11436
|
+
}
|
|
11437
|
+
|
|
11438
|
+
h1 {
|
|
11439
|
+
margin: 0;
|
|
11440
|
+
font-family: var(--font-display);
|
|
11441
|
+
font-size: clamp(2.4rem, 7vw, 6.8rem);
|
|
11442
|
+
line-height: 0.82;
|
|
11443
|
+
letter-spacing: -0.08em;
|
|
11444
|
+
text-transform: uppercase;
|
|
11445
|
+
max-width: 11ch;
|
|
11446
|
+
}
|
|
11447
|
+
|
|
11448
|
+
.dek {
|
|
11449
|
+
margin: 12px 0 0;
|
|
11450
|
+
max-width: 78ch;
|
|
11451
|
+
color: var(--muted);
|
|
11452
|
+
font-size: clamp(0.98rem, 1.8vw, 1.18rem);
|
|
11453
|
+
line-height: 1.45;
|
|
11454
|
+
}
|
|
11455
|
+
|
|
11456
|
+
.controls {
|
|
11457
|
+
display: flex;
|
|
11458
|
+
flex-wrap: wrap;
|
|
11459
|
+
gap: 10px;
|
|
11460
|
+
align-items: center;
|
|
11461
|
+
justify-content: space-between;
|
|
11462
|
+
}
|
|
11463
|
+
|
|
11464
|
+
.filter-row {
|
|
11465
|
+
display: flex;
|
|
11466
|
+
gap: 8px;
|
|
11467
|
+
flex-wrap: wrap;
|
|
11468
|
+
}
|
|
11469
|
+
|
|
11470
|
+
.chip, .button {
|
|
11471
|
+
border: 2px solid var(--ink);
|
|
11472
|
+
background: var(--surface);
|
|
11473
|
+
color: var(--ink);
|
|
11474
|
+
min-height: 40px;
|
|
11475
|
+
padding: 8px 13px;
|
|
11476
|
+
box-shadow: 3px 3px 0 var(--ink);
|
|
11477
|
+
cursor: pointer;
|
|
11478
|
+
transition: transform 140ms ease, box-shadow 140ms ease, background 140ms ease;
|
|
11479
|
+
}
|
|
11480
|
+
|
|
11481
|
+
.chip[aria-pressed="true"], .button.primary {
|
|
11482
|
+
background: var(--ink);
|
|
11483
|
+
color: var(--paper);
|
|
11484
|
+
}
|
|
11485
|
+
|
|
11486
|
+
@media (hover: hover) {
|
|
11487
|
+
.chip:hover, .button:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--ink); }
|
|
11488
|
+
}
|
|
11489
|
+
|
|
11490
|
+
.search {
|
|
11491
|
+
display: flex;
|
|
11492
|
+
align-items: center;
|
|
11493
|
+
gap: 10px;
|
|
11494
|
+
min-width: min(100%, 380px);
|
|
11495
|
+
border: 2px solid var(--ink);
|
|
11496
|
+
background: var(--surface);
|
|
11497
|
+
box-shadow: 3px 3px 0 var(--ink);
|
|
11498
|
+
padding: 8px 12px;
|
|
11499
|
+
}
|
|
11500
|
+
|
|
11501
|
+
.search input {
|
|
11502
|
+
width: 100%;
|
|
11503
|
+
border: 0;
|
|
11504
|
+
background: transparent;
|
|
11505
|
+
color: var(--ink);
|
|
11506
|
+
outline: 0;
|
|
11507
|
+
}
|
|
11508
|
+
|
|
11509
|
+
.dashboard {
|
|
11510
|
+
display: grid;
|
|
11511
|
+
grid-template-columns: minmax(0, 1fr);
|
|
11512
|
+
gap: clamp(18px, 3vw, 34px);
|
|
11513
|
+
margin-top: clamp(22px, 4vw, 48px);
|
|
11514
|
+
}
|
|
11515
|
+
|
|
11516
|
+
.metrics {
|
|
11517
|
+
display: grid;
|
|
11518
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
11519
|
+
gap: 12px;
|
|
11520
|
+
}
|
|
11521
|
+
|
|
11522
|
+
.metric {
|
|
11523
|
+
min-height: 118px;
|
|
11524
|
+
border: 2px solid var(--ink);
|
|
11525
|
+
background: var(--surface);
|
|
11526
|
+
box-shadow: var(--shadow);
|
|
11527
|
+
padding: 16px;
|
|
11528
|
+
display: flex;
|
|
11529
|
+
flex-direction: column;
|
|
11530
|
+
justify-content: space-between;
|
|
11531
|
+
}
|
|
11532
|
+
|
|
11533
|
+
.metric b {
|
|
11534
|
+
font-family: var(--font-display);
|
|
11535
|
+
font-size: clamp(2rem, 5vw, 4.2rem);
|
|
11536
|
+
line-height: 0.9;
|
|
11537
|
+
letter-spacing: -0.05em;
|
|
11538
|
+
}
|
|
11539
|
+
|
|
11540
|
+
.metric span {
|
|
11541
|
+
color: var(--muted);
|
|
11542
|
+
text-transform: uppercase;
|
|
11543
|
+
font-size: 0.72rem;
|
|
11544
|
+
letter-spacing: 0.14em;
|
|
11545
|
+
font-weight: 800;
|
|
11546
|
+
}
|
|
11547
|
+
|
|
11548
|
+
.panel {
|
|
11549
|
+
border: 2px solid var(--ink);
|
|
11550
|
+
background: oklch(98% 0.014 88 / 0.96);
|
|
11551
|
+
box-shadow: 8px 8px 0 oklch(18% 0.026 70 / 0.22);
|
|
11552
|
+
min-width: 0;
|
|
11553
|
+
}
|
|
11554
|
+
|
|
11555
|
+
.panel-head {
|
|
11556
|
+
display: flex;
|
|
11557
|
+
align-items: baseline;
|
|
11558
|
+
justify-content: space-between;
|
|
11559
|
+
gap: 12px;
|
|
11560
|
+
padding: 14px 16px;
|
|
11561
|
+
border-bottom: 2px solid var(--ink);
|
|
11562
|
+
background: var(--paper-2);
|
|
11563
|
+
}
|
|
11564
|
+
|
|
11565
|
+
.panel-head h2 {
|
|
11566
|
+
margin: 0;
|
|
11567
|
+
font-family: var(--font-display);
|
|
11568
|
+
font-size: clamp(1.2rem, 2vw, 1.7rem);
|
|
11569
|
+
letter-spacing: -0.03em;
|
|
11570
|
+
text-transform: uppercase;
|
|
11571
|
+
}
|
|
11572
|
+
|
|
11573
|
+
.panel-head small {
|
|
11574
|
+
color: var(--muted);
|
|
11575
|
+
font-family: var(--font-mono);
|
|
11576
|
+
font-size: 0.75rem;
|
|
11577
|
+
}
|
|
11578
|
+
|
|
11579
|
+
.panel-body {
|
|
11580
|
+
padding: 14px;
|
|
11581
|
+
max-height: min(62vh, 720px);
|
|
11582
|
+
overflow: auto;
|
|
11583
|
+
}
|
|
11584
|
+
|
|
11585
|
+
.stream-grid, .emitter-grid, .provider-grid {
|
|
11586
|
+
display: grid;
|
|
11587
|
+
gap: 12px;
|
|
11588
|
+
}
|
|
11589
|
+
|
|
11590
|
+
.record {
|
|
11591
|
+
border-left: 4px solid var(--line);
|
|
11592
|
+
background: color-mix(in oklch, var(--surface), var(--paper) 24%);
|
|
11593
|
+
padding: 11px 12px;
|
|
11594
|
+
}
|
|
11595
|
+
|
|
11596
|
+
.record strong {
|
|
11597
|
+
display: inline-flex;
|
|
11598
|
+
gap: 8px;
|
|
11599
|
+
align-items: baseline;
|
|
11600
|
+
font-family: var(--font-display);
|
|
11601
|
+
letter-spacing: -0.01em;
|
|
11602
|
+
}
|
|
11603
|
+
|
|
11604
|
+
.meta {
|
|
11605
|
+
color: var(--muted);
|
|
11606
|
+
font-family: var(--font-mono);
|
|
11607
|
+
font-size: 0.72rem;
|
|
11608
|
+
overflow-wrap: anywhere;
|
|
11609
|
+
}
|
|
11610
|
+
|
|
11611
|
+
.entry {
|
|
11612
|
+
margin-top: 8px;
|
|
11613
|
+
padding-top: 8px;
|
|
11614
|
+
border-top: 1px dashed var(--line);
|
|
11615
|
+
font-family: var(--font-mono);
|
|
11616
|
+
font-size: 0.78rem;
|
|
11617
|
+
line-height: 1.45;
|
|
11618
|
+
white-space: pre-wrap;
|
|
11619
|
+
overflow-wrap: anywhere;
|
|
11620
|
+
}
|
|
11621
|
+
|
|
11622
|
+
.timeline {
|
|
11623
|
+
display: grid;
|
|
11624
|
+
gap: 9px;
|
|
11625
|
+
}
|
|
11626
|
+
|
|
11627
|
+
.tick {
|
|
11628
|
+
display: grid;
|
|
11629
|
+
grid-template-columns: 90px minmax(0, 1fr);
|
|
11630
|
+
gap: 12px;
|
|
11631
|
+
border-bottom: 1px dashed var(--line);
|
|
11632
|
+
padding: 9px 0;
|
|
11633
|
+
}
|
|
11634
|
+
|
|
11635
|
+
.tick time {
|
|
11636
|
+
color: var(--muted);
|
|
11637
|
+
font-family: var(--font-mono);
|
|
11638
|
+
font-size: 0.72rem;
|
|
11639
|
+
}
|
|
11640
|
+
|
|
11641
|
+
.badge {
|
|
11642
|
+
display: inline-flex;
|
|
11643
|
+
align-items: center;
|
|
11644
|
+
gap: 6px;
|
|
11645
|
+
padding: 2px 7px;
|
|
11646
|
+
border: 1px solid var(--ink);
|
|
11647
|
+
background: var(--paper);
|
|
11648
|
+
font-family: var(--font-mono);
|
|
11649
|
+
font-size: 0.68rem;
|
|
11650
|
+
text-transform: uppercase;
|
|
11651
|
+
letter-spacing: 0.08em;
|
|
11652
|
+
}
|
|
11653
|
+
|
|
11654
|
+
.badge.error { background: color-mix(in oklch, var(--red), var(--paper) 72%); }
|
|
11655
|
+
.badge.warning { background: color-mix(in oklch, var(--amber), var(--paper) 54%); }
|
|
11656
|
+
.badge.ready, .badge.running, .badge.success { background: color-mix(in oklch, var(--green), var(--paper) 70%); }
|
|
11657
|
+
.badge.info, .badge.debug { background: color-mix(in oklch, var(--blue), var(--paper) 76%); }
|
|
11658
|
+
|
|
11659
|
+
.details {
|
|
11660
|
+
margin-top: 6px;
|
|
11661
|
+
color: var(--ink);
|
|
11662
|
+
font-family: var(--font-mono);
|
|
11663
|
+
font-size: 0.78rem;
|
|
11664
|
+
white-space: pre-wrap;
|
|
11665
|
+
overflow-wrap: anywhere;
|
|
11666
|
+
}
|
|
11667
|
+
|
|
11668
|
+
.empty {
|
|
11669
|
+
border: 2px dashed var(--line);
|
|
11670
|
+
padding: 18px;
|
|
11671
|
+
color: var(--muted);
|
|
11672
|
+
background: color-mix(in oklch, var(--surface), var(--paper) 45%);
|
|
11673
|
+
}
|
|
11674
|
+
|
|
11675
|
+
.footer {
|
|
11676
|
+
margin-top: 24px;
|
|
11677
|
+
color: var(--muted);
|
|
11678
|
+
font-size: 0.86rem;
|
|
11679
|
+
}
|
|
11680
|
+
|
|
11681
|
+
@media (min-width: 780px) {
|
|
11682
|
+
.masthead { grid-template-columns: minmax(0, 1fr) auto; }
|
|
11683
|
+
.dashboard { grid-template-columns: minmax(280px, 0.62fr) minmax(0, 1fr); align-items: start; }
|
|
11684
|
+
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
11685
|
+
.timeline-panel { grid-column: 1 / -1; }
|
|
11686
|
+
}
|
|
11687
|
+
|
|
11688
|
+
@media (min-width: 1180px) {
|
|
11689
|
+
.dashboard { grid-template-columns: 360px minmax(0, 1fr) 420px; }
|
|
11690
|
+
.timeline-panel { grid-column: auto; }
|
|
11691
|
+
.metrics { grid-template-columns: 1fr; }
|
|
11692
|
+
}
|
|
11693
|
+
|
|
11694
|
+
@media (prefers-reduced-motion: reduce) {
|
|
11695
|
+
*, *::before, *::after {
|
|
11696
|
+
animation-duration: 0.01ms !important;
|
|
11697
|
+
animation-iteration-count: 1 !important;
|
|
11698
|
+
scroll-behavior: auto !important;
|
|
11699
|
+
transition-duration: 0.01ms !important;
|
|
11700
|
+
}
|
|
11701
|
+
}
|
|
11702
|
+
</style>
|
|
11703
|
+
</head>
|
|
11704
|
+
<body>
|
|
11705
|
+
<main class="shell">
|
|
11706
|
+
<header class="masthead">
|
|
11707
|
+
<div class="title-wrap">
|
|
11708
|
+
<div class="mark" aria-hidden="true">\u203B</div>
|
|
11709
|
+
<div>
|
|
11710
|
+
<h1>Tap flight recorder</h1>
|
|
11711
|
+
<p class="dek">A live canvas for streams, emitters, provider gateway state, injection queues, session events, and tap diagnostics. Bounded, redacted, and built for incident-speed inspection.</p>
|
|
11712
|
+
</div>
|
|
11713
|
+
</div>
|
|
11714
|
+
<div class="controls" aria-label="Diagnostics controls">
|
|
11715
|
+
<div class="filter-row" role="group" aria-label="Timeline filter">
|
|
11716
|
+
<button class="chip" data-filter="all" aria-pressed="true">All</button>
|
|
11717
|
+
<button class="chip" data-filter="streams" aria-pressed="false">Streams</button>
|
|
11718
|
+
<button class="chip" data-filter="logs" aria-pressed="false">Logs</button>
|
|
11719
|
+
<button class="chip" data-filter="session" aria-pressed="false">Session</button>
|
|
11720
|
+
<button class="chip" data-filter="runtime" aria-pressed="false">Runtime</button>
|
|
11721
|
+
</div>
|
|
11722
|
+
<label class="search">
|
|
11723
|
+
<span aria-hidden="true">\u2315</span>
|
|
11724
|
+
<input id="search" type="search" placeholder="Filter evidence..." autocomplete="off" />
|
|
11725
|
+
</label>
|
|
11726
|
+
<button id="pause" class="button" type="button">Pause</button>
|
|
11727
|
+
<button id="refresh" class="button primary" type="button">Refresh</button>
|
|
11728
|
+
</div>
|
|
11729
|
+
</header>
|
|
11730
|
+
|
|
11731
|
+
<section class="dashboard" aria-live="polite">
|
|
11732
|
+
<aside>
|
|
11733
|
+
<div class="metrics" id="metrics"></div>
|
|
11734
|
+
<p class="footer" id="heartbeat">Waiting for first snapshot...</p>
|
|
11735
|
+
</aside>
|
|
11736
|
+
|
|
11737
|
+
<section class="panel">
|
|
11738
|
+
<div class="panel-head">
|
|
11739
|
+
<h2>Streams and emitters</h2>
|
|
11740
|
+
<small id="stream-count">0 streams</small>
|
|
11741
|
+
</div>
|
|
11742
|
+
<div class="panel-body">
|
|
11743
|
+
<div class="stream-grid" id="streams"></div>
|
|
11744
|
+
</div>
|
|
11745
|
+
</section>
|
|
11746
|
+
|
|
11747
|
+
<section class="panel">
|
|
11748
|
+
<div class="panel-head">
|
|
11749
|
+
<h2>Providers</h2>
|
|
11750
|
+
<small id="provider-count">0 providers</small>
|
|
11751
|
+
</div>
|
|
11752
|
+
<div class="panel-body">
|
|
11753
|
+
<div class="provider-grid" id="providers"></div>
|
|
11754
|
+
</div>
|
|
11755
|
+
</section>
|
|
11756
|
+
|
|
11757
|
+
<section class="panel timeline-panel">
|
|
11758
|
+
<div class="panel-head">
|
|
11759
|
+
<h2>Evidence timeline</h2>
|
|
11760
|
+
<small id="timeline-count">0 events</small>
|
|
11761
|
+
</div>
|
|
11762
|
+
<div class="panel-body">
|
|
11763
|
+
<div class="timeline" id="timeline"></div>
|
|
11764
|
+
</div>
|
|
11765
|
+
</section>
|
|
11766
|
+
</section>
|
|
11767
|
+
</main>
|
|
11768
|
+
|
|
11769
|
+
<script>
|
|
11770
|
+
const state = { snapshot: null, paused: false, filter: "all", query: "" };
|
|
11771
|
+
const el = (id) => document.getElementById(id);
|
|
11772
|
+
const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch]));
|
|
11773
|
+
const timeOnly = (value) => {
|
|
11774
|
+
const date = new Date(value);
|
|
11775
|
+
return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], { hour12: false });
|
|
11776
|
+
};
|
|
11777
|
+
const compactJson = (value) => {
|
|
11778
|
+
if (value === null || value === undefined) return "";
|
|
11779
|
+
if (typeof value === "string") return value;
|
|
11780
|
+
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
|
11781
|
+
};
|
|
11782
|
+
|
|
11783
|
+
function metric(label, value, tone = "") {
|
|
11784
|
+
return '<article class="metric ' + tone + '"><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value) + '</b></article>';
|
|
11785
|
+
}
|
|
11786
|
+
|
|
11787
|
+
function renderMetrics(snapshot) {
|
|
11788
|
+
const running = snapshot.emitters?.running?.length ?? 0;
|
|
11789
|
+
const configured = snapshot.emitters?.configured?.length ?? 0;
|
|
11790
|
+
const streams = snapshot.streams?.length ?? 0;
|
|
11791
|
+
const providers = snapshot.gateway?.providers?.length ?? 0;
|
|
11792
|
+
const queue = snapshot.notifications?.queueSize ?? 0;
|
|
11793
|
+
const sessionEvents = snapshot.diagnostics?.stats?.sessionEvents?.retained ?? 0;
|
|
11794
|
+
el("metrics").innerHTML = [
|
|
11795
|
+
metric("streams", streams),
|
|
11796
|
+
metric("running emitters", running),
|
|
11797
|
+
metric("providers", providers),
|
|
11798
|
+
metric("queued injections", queue),
|
|
11799
|
+
metric("configured emitters", configured),
|
|
11800
|
+
metric("session events", sessionEvents)
|
|
11801
|
+
].join("");
|
|
11802
|
+
el("heartbeat").textContent = "Snapshot " + timeOnly(snapshot.generatedAt) + " | pid " + (snapshot.process?.pid ?? "?") + " | gateway " + (snapshot.gateway?.running ? "ready" : "stopped");
|
|
11803
|
+
}
|
|
11804
|
+
|
|
11805
|
+
function renderStreams(snapshot) {
|
|
11806
|
+
const streams = snapshot.streams ?? [];
|
|
11807
|
+
el("stream-count").textContent = streams.length + " streams";
|
|
11808
|
+
if (streams.length === 0) {
|
|
11809
|
+
el("streams").innerHTML = '<div class="empty">No streams are currently retained.</div>';
|
|
11810
|
+
return;
|
|
11811
|
+
}
|
|
11812
|
+
const emitterRows = [...(snapshot.emitters?.running ?? []), ...(snapshot.emitters?.configured ?? [])]
|
|
11813
|
+
.map((emitter) => '<div class="record"><strong>' + escapeHtml(emitter.name) + ' <span class="badge ' + escapeHtml(emitter.status) + '">' + escapeHtml(emitter.status) + '</span></strong><div class="meta">' + escapeHtml(emitter.emitterType) + ' | ' + escapeHtml(emitter.runSchedule) + ' | stream=' + escapeHtml(emitter.stream) + '</div><div class="meta">lines=' + escapeHtml(emitter.lineCount ?? 0) + ' dropped=' + escapeHtml(emitter.droppedLineCount ?? 0) + '</div></div>')
|
|
11814
|
+
.join("");
|
|
11815
|
+
const streamRows = streams.map((stream) => {
|
|
11816
|
+
const latest = (stream.entries ?? []).slice(-3).reverse().map((entry) => '<div class="entry"><span class="meta">' + escapeHtml(timeOnly(entry.timestamp)) + ' ' + escapeHtml(entry.source) + (entry.monitorName ? ' / ' + escapeHtml(entry.monitorName) : '') + '</span>\\n' + escapeHtml(entry.text) + '</div>').join("");
|
|
11817
|
+
return '<div class="record"><strong>' + escapeHtml(stream.name) + ' <span class="badge ' + (stream.sessionInjector?.enabled ? 'ready' : 'info') + '">' + (stream.sessionInjector?.enabled ? 'injector on' : 'kept') + '</span></strong><div class="meta">' + escapeHtml(stream.entries?.length ?? 0) + ' retained entries | delivery=' + escapeHtml(stream.sessionInjector?.delivery ?? 'surface') + '</div>' + (latest || '<div class="entry">No entries retained for this stream.</div>') + '</div>';
|
|
11818
|
+
}).join("");
|
|
11819
|
+
el("streams").innerHTML = streamRows + (emitterRows ? '<div class="panel-head" style="margin:14px -14px 12px"><h2>Emitter roll call</h2><small>' + ((snapshot.emitters?.running?.length ?? 0) + (snapshot.emitters?.configured?.length ?? 0)) + ' emitters</small></div><div class="emitter-grid">' + emitterRows + '</div>' : "");
|
|
11820
|
+
}
|
|
11821
|
+
|
|
11822
|
+
function renderProviders(snapshot) {
|
|
11823
|
+
const gateway = snapshot.gateway ?? {};
|
|
11824
|
+
const providers = gateway.providers ?? [];
|
|
11825
|
+
el("provider-count").textContent = providers.length + " providers";
|
|
11826
|
+
const gatewayRecord = '<div class="record"><strong>gateway <span class="badge ' + (gateway.running ? 'ready' : 'error') + '">' + (gateway.running ? 'running' : 'stopped') + '</span></strong><div class="meta">port=' + escapeHtml(gateway.port ?? "?") + ' connections=' + escapeHtml(gateway.connectionCount ?? 0) + ' reloadPending=' + escapeHtml(gateway.reloadPending ?? false) + ' token=' + (gateway.tokenPresent ? 'present' : 'absent') + '</div></div>';
|
|
11827
|
+
if (providers.length === 0) {
|
|
11828
|
+
el("providers").innerHTML = gatewayRecord + '<div class="empty">No external providers are bound. The canvas still shows tap-native streams and emitters.</div>';
|
|
11829
|
+
return;
|
|
11830
|
+
}
|
|
11831
|
+
const rows = providers.map((provider) => {
|
|
11832
|
+
const tools = (provider.tools ?? []).map((tool) => tool.name).join(", ") || "no tools";
|
|
11833
|
+
return '<div class="record"><strong>' + escapeHtml(provider.name) + ' <span class="badge ready">' + escapeHtml(provider.id) + '</span></strong><div class="meta">session=' + escapeHtml(provider.sessionId ?? "none") + ' | tools=' + escapeHtml(provider.toolCount ?? 0) + '</div><div class="details">' + escapeHtml(tools) + '</div></div>';
|
|
11834
|
+
}).join("");
|
|
11835
|
+
el("providers").innerHTML = gatewayRecord + rows;
|
|
11836
|
+
}
|
|
11837
|
+
|
|
11838
|
+
function collectTimeline(snapshot) {
|
|
11839
|
+
const items = [];
|
|
11840
|
+
for (const stream of snapshot.streams ?? []) {
|
|
11841
|
+
for (const entry of stream.entries ?? []) {
|
|
11842
|
+
items.push({ group: "streams", timestamp: entry.timestamp, source: "stream/" + stream.name, level: "info", message: entry.text, detail: entry.monitorName ? entry.monitorName + " " + (entry.stream ?? "") : entry.source });
|
|
11843
|
+
}
|
|
11844
|
+
}
|
|
11845
|
+
for (const log of snapshot.diagnostics?.logs ?? []) {
|
|
11846
|
+
items.push({ group: "logs", timestamp: log.timestamp, source: log.source, level: log.level, message: log.message, detail: compactJson(log.metadata) });
|
|
11847
|
+
}
|
|
11848
|
+
for (const event of snapshot.diagnostics?.sessionEvents ?? []) {
|
|
11849
|
+
items.push({ group: "session", timestamp: event.timestamp, source: "session", level: "debug", message: event.type, detail: compactJson(event.data) });
|
|
11850
|
+
}
|
|
11851
|
+
for (const event of snapshot.diagnostics?.runtimeEvents ?? []) {
|
|
11852
|
+
items.push({ group: "runtime", timestamp: event.timestamp, source: event.type, level: "info", message: event.message, detail: compactJson(event.metadata) });
|
|
11853
|
+
}
|
|
11854
|
+
return items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
11855
|
+
}
|
|
11856
|
+
|
|
11857
|
+
function renderTimeline(snapshot) {
|
|
11858
|
+
const query = state.query.trim().toLowerCase();
|
|
11859
|
+
const items = collectTimeline(snapshot).filter((item) => {
|
|
11860
|
+
if (state.filter !== "all" && item.group !== state.filter) return false;
|
|
11861
|
+
if (!query) return true;
|
|
11862
|
+
return [item.source, item.level, item.message, item.detail].join(" ").toLowerCase().includes(query);
|
|
11863
|
+
}).slice(0, 260);
|
|
11864
|
+
el("timeline-count").textContent = items.length + " visible";
|
|
11865
|
+
if (items.length === 0) {
|
|
11866
|
+
el("timeline").innerHTML = '<div class="empty">No evidence matches the current filter.</div>';
|
|
11867
|
+
return;
|
|
11868
|
+
}
|
|
11869
|
+
el("timeline").innerHTML = items.map((item) => '<div class="tick"><time>' + escapeHtml(timeOnly(item.timestamp)) + '</time><div><span class="badge ' + escapeHtml(item.level) + '">' + escapeHtml(item.group) + '</span> <span class="meta">' + escapeHtml(item.source) + '</span><div><strong>' + escapeHtml(item.message) + '</strong></div>' + (item.detail ? '<div class="details">' + escapeHtml(item.detail) + '</div>' : '') + '</div></div>').join("");
|
|
11870
|
+
}
|
|
11871
|
+
|
|
11872
|
+
function render(snapshot) {
|
|
11873
|
+
state.snapshot = snapshot;
|
|
11874
|
+
renderMetrics(snapshot);
|
|
11875
|
+
renderStreams(snapshot);
|
|
11876
|
+
renderProviders(snapshot);
|
|
11877
|
+
renderTimeline(snapshot);
|
|
11878
|
+
}
|
|
11879
|
+
|
|
11880
|
+
async function refresh() {
|
|
11881
|
+
if (state.paused) return;
|
|
11882
|
+
const response = await fetch("/api/snapshot", { cache: "no-store" });
|
|
11883
|
+
if (response.ok) render(await response.json());
|
|
11884
|
+
}
|
|
11885
|
+
|
|
11886
|
+
function connectEvents() {
|
|
11887
|
+
if (!("EventSource" in window)) {
|
|
11888
|
+
setInterval(refresh, 1800);
|
|
11889
|
+
refresh();
|
|
11890
|
+
return;
|
|
11891
|
+
}
|
|
11892
|
+
const source = new EventSource("/events");
|
|
11893
|
+
source.addEventListener("snapshot", (event) => {
|
|
11894
|
+
if (!state.paused) render(JSON.parse(event.data));
|
|
11895
|
+
});
|
|
11896
|
+
source.onerror = () => {
|
|
11897
|
+
setTimeout(refresh, 2000);
|
|
11898
|
+
};
|
|
11899
|
+
}
|
|
11900
|
+
|
|
11901
|
+
document.querySelectorAll("[data-filter]").forEach((button) => {
|
|
11902
|
+
button.addEventListener("click", () => {
|
|
11903
|
+
state.filter = button.dataset.filter;
|
|
11904
|
+
document.querySelectorAll("[data-filter]").forEach((item) => item.setAttribute("aria-pressed", String(item === button)));
|
|
11905
|
+
if (state.snapshot) renderTimeline(state.snapshot);
|
|
11906
|
+
});
|
|
11907
|
+
});
|
|
11908
|
+
el("search").addEventListener("input", (event) => {
|
|
11909
|
+
state.query = event.target.value;
|
|
11910
|
+
if (state.snapshot) renderTimeline(state.snapshot);
|
|
11911
|
+
});
|
|
11912
|
+
el("pause").addEventListener("click", () => {
|
|
11913
|
+
state.paused = !state.paused;
|
|
11914
|
+
el("pause").textContent = state.paused ? "Resume" : "Pause";
|
|
11915
|
+
if (!state.paused) refresh();
|
|
11916
|
+
});
|
|
11917
|
+
el("refresh").addEventListener("click", () => refresh());
|
|
11918
|
+
connectEvents();
|
|
11919
|
+
</script>
|
|
11920
|
+
</body>
|
|
11921
|
+
</html>`;
|
|
11922
|
+
}
|
|
11923
|
+
function createTapDiagnosticsCanvas({ getSnapshot, diagnostics: diagnostics2 } = {}) {
|
|
11924
|
+
const instances = /* @__PURE__ */ new Map();
|
|
11925
|
+
function snapshot(options = {}) {
|
|
11926
|
+
return typeof getSnapshot === "function" ? getSnapshot(sanitizeSnapshotOptions(options)) : { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), error: "No diagnostics snapshot provider configured." };
|
|
11927
|
+
}
|
|
11928
|
+
function log(message, level = "info", metadata = {}) {
|
|
11929
|
+
diagnostics2?.log?.("canvas", message, { level, metadata });
|
|
11930
|
+
}
|
|
11931
|
+
async function startServer(instanceId) {
|
|
11932
|
+
const server = createServer((req, res) => {
|
|
11933
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
11934
|
+
if (url.pathname === "/") {
|
|
11935
|
+
textResponse(res, 200, createHtml(), "text/html; charset=utf-8");
|
|
11936
|
+
return;
|
|
11937
|
+
}
|
|
11938
|
+
if (url.pathname === "/api/snapshot") {
|
|
11939
|
+
jsonResponse(res, 200, snapshot({ limit: url.searchParams.get("limit") }));
|
|
11940
|
+
return;
|
|
11941
|
+
}
|
|
11942
|
+
if (url.pathname === "/events") {
|
|
11943
|
+
res.writeHead(200, {
|
|
11944
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
11945
|
+
"Cache-Control": "no-store",
|
|
11946
|
+
"Connection": "keep-alive",
|
|
11947
|
+
"X-Accel-Buffering": "no"
|
|
11948
|
+
});
|
|
11949
|
+
const send = () => {
|
|
11950
|
+
res.write(`event: snapshot
|
|
11951
|
+
data: ${JSON.stringify(snapshot())}
|
|
11952
|
+
|
|
11953
|
+
`);
|
|
11954
|
+
};
|
|
11955
|
+
send();
|
|
11956
|
+
const interval = setInterval(send, DEFAULT_REFRESH_MS);
|
|
11957
|
+
req.on("close", () => clearInterval(interval));
|
|
11958
|
+
return;
|
|
11959
|
+
}
|
|
11960
|
+
jsonResponse(res, 404, { error: "not_found" });
|
|
11961
|
+
});
|
|
11962
|
+
await new Promise((resolve, reject) => {
|
|
11963
|
+
server.once("error", reject);
|
|
11964
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
11965
|
+
});
|
|
11966
|
+
const address = server.address();
|
|
11967
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
11968
|
+
const entry = { server, url: `http://127.0.0.1:${port}/` };
|
|
11969
|
+
instances.set(instanceId, entry);
|
|
11970
|
+
log(`Diagnostics canvas server started for instance '${instanceId}'.`, "info", { url: entry.url });
|
|
11971
|
+
return entry;
|
|
11972
|
+
}
|
|
11973
|
+
async function stopServer(instanceId) {
|
|
11974
|
+
const entry = instances.get(instanceId);
|
|
11975
|
+
if (!entry) {
|
|
11976
|
+
return;
|
|
11977
|
+
}
|
|
11978
|
+
instances.delete(instanceId);
|
|
11979
|
+
await new Promise((resolve) => entry.server.close(() => resolve()));
|
|
11980
|
+
log(`Diagnostics canvas server stopped for instance '${instanceId}'.`);
|
|
11981
|
+
}
|
|
11982
|
+
return createCanvas({
|
|
11983
|
+
id: TAP_DIAGNOSTICS_CANVAS_ID,
|
|
11984
|
+
displayName: "Tap diagnostics",
|
|
11985
|
+
description: "Live tap flight recorder for streams, emitters, provider gateway state, logs, and session events.",
|
|
11986
|
+
inputSchema: {
|
|
11987
|
+
type: "object",
|
|
11988
|
+
properties: {
|
|
11989
|
+
limit: { type: "integer", minimum: 10, maximum: 300, description: "Maximum retained rows to show per diagnostics section." }
|
|
11990
|
+
}
|
|
11991
|
+
},
|
|
11992
|
+
actions: [
|
|
11993
|
+
{
|
|
11994
|
+
name: "refresh_snapshot",
|
|
11995
|
+
description: "Return a fresh summary of the tap diagnostics canvas data.",
|
|
11996
|
+
inputSchema: {
|
|
11997
|
+
type: "object",
|
|
11998
|
+
properties: {
|
|
11999
|
+
limit: { type: "integer", minimum: 10, maximum: 300 }
|
|
12000
|
+
}
|
|
12001
|
+
},
|
|
12002
|
+
handler: async ({ input }) => ({
|
|
12003
|
+
ok: true,
|
|
12004
|
+
summary: summarizeSnapshot(snapshot(input))
|
|
12005
|
+
})
|
|
12006
|
+
},
|
|
12007
|
+
{
|
|
12008
|
+
name: "export_snapshot",
|
|
12009
|
+
description: "Return the current tap diagnostics snapshot as structured JSON.",
|
|
12010
|
+
inputSchema: {
|
|
12011
|
+
type: "object",
|
|
12012
|
+
properties: {
|
|
12013
|
+
limit: { type: "integer", minimum: 10, maximum: 300 }
|
|
12014
|
+
}
|
|
12015
|
+
},
|
|
12016
|
+
handler: async ({ input }) => snapshot(input)
|
|
12017
|
+
}
|
|
12018
|
+
],
|
|
12019
|
+
open: async (ctx) => {
|
|
12020
|
+
let entry = instances.get(ctx.instanceId);
|
|
12021
|
+
if (!entry) {
|
|
12022
|
+
entry = await startServer(ctx.instanceId);
|
|
12023
|
+
}
|
|
12024
|
+
return {
|
|
12025
|
+
title: "Tap diagnostics",
|
|
12026
|
+
status: "live flight recorder",
|
|
12027
|
+
url: entry.url
|
|
12028
|
+
};
|
|
12029
|
+
},
|
|
12030
|
+
onClose: async (ctx) => {
|
|
12031
|
+
await stopServer(ctx.instanceId);
|
|
12032
|
+
}
|
|
12033
|
+
});
|
|
12034
|
+
}
|
|
12035
|
+
|
|
10895
12036
|
// src/tap-runtime/runtime-factory.mjs
|
|
10896
12037
|
var PROVIDER_SHUTDOWN_DEADLINE_MS = 1e4;
|
|
10897
12038
|
var EMITTER_SHUTDOWN_WAIT_TIMEOUT_MS = 1e4;
|
|
@@ -10917,10 +12058,15 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
10917
12058
|
...options.runtimeServiceOptions ?? {},
|
|
10918
12059
|
cwd: options.cwd,
|
|
10919
12060
|
session: options.session,
|
|
12061
|
+
diagnostics: options.diagnostics,
|
|
10920
12062
|
shutdownSession: () => handleSessionShutdown(activeSession)
|
|
10921
12063
|
});
|
|
10922
|
-
|
|
12064
|
+
function logRuntime(message, options2 = {}) {
|
|
12065
|
+
runtimeService.diagnostics?.log?.("runtime", message, options2);
|
|
12066
|
+
process.stderr.write(`[tap-runtime] ${message}
|
|
10923
12067
|
`);
|
|
12068
|
+
}
|
|
12069
|
+
logRuntime(`init \u2014 cwd=${runtimeService.session.getBaseCwd()}`);
|
|
10924
12070
|
const tools = createTools({ tools: runtimeService.tools });
|
|
10925
12071
|
const hooks = createHooks({ runtime: runtimeService.hooks });
|
|
10926
12072
|
const tapToolsFn = () => tools;
|
|
@@ -10930,8 +12076,19 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
10930
12076
|
deliverPush: runtimeService.provider.deliverPush,
|
|
10931
12077
|
log: runtimeService.provider.log
|
|
10932
12078
|
});
|
|
10933
|
-
|
|
10934
|
-
|
|
12079
|
+
logRuntime("gateway created");
|
|
12080
|
+
function getDiagnosticSnapshot(options2 = {}) {
|
|
12081
|
+
return runtimeService.diagnostics.snapshot(options2, {
|
|
12082
|
+
gateway: typeof gateway.getDiagnosticState === "function" ? gateway.getDiagnosticState() : null,
|
|
12083
|
+
tools: gateway.isRunning() ? gateway.getAllTools(tools) : tools
|
|
12084
|
+
});
|
|
12085
|
+
}
|
|
12086
|
+
const canvases = [
|
|
12087
|
+
createTapDiagnosticsCanvas({
|
|
12088
|
+
getSnapshot: getDiagnosticSnapshot,
|
|
12089
|
+
diagnostics: runtimeService.diagnostics
|
|
12090
|
+
})
|
|
12091
|
+
];
|
|
10935
12092
|
gateway.onToolsChanged(runtimeService.provider.replaceSessionTools);
|
|
10936
12093
|
let cleanupShutdownListener = () => {
|
|
10937
12094
|
};
|
|
@@ -10958,7 +12115,7 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
10958
12115
|
return;
|
|
10959
12116
|
}
|
|
10960
12117
|
handled = true;
|
|
10961
|
-
|
|
12118
|
+
logRuntime("session.shutdown received \u2014 stopping runtime");
|
|
10962
12119
|
void handleSessionShutdown(session2).catch((error) => {
|
|
10963
12120
|
logShutdownFailure(session2, error);
|
|
10964
12121
|
});
|
|
@@ -11027,19 +12184,15 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
11027
12184
|
return;
|
|
11028
12185
|
}
|
|
11029
12186
|
try {
|
|
11030
|
-
|
|
11031
|
-
`);
|
|
12187
|
+
logRuntime("starting gateway...");
|
|
11032
12188
|
gateway.start();
|
|
11033
|
-
|
|
11034
|
-
`);
|
|
12189
|
+
logRuntime("gateway start requested");
|
|
11035
12190
|
} catch (err) {
|
|
11036
|
-
|
|
11037
|
-
`);
|
|
12191
|
+
logRuntime(`gateway start request failed: ${err?.message ?? err}`, { level: "warning" });
|
|
11038
12192
|
}
|
|
11039
12193
|
}
|
|
11040
12194
|
function attachSession(nextSession) {
|
|
11041
|
-
|
|
11042
|
-
`);
|
|
12195
|
+
logRuntime(`attachSession \u2014 id=${nextSession?.id ?? "(none)"}`);
|
|
11043
12196
|
activeSession = nextSession ?? null;
|
|
11044
12197
|
if (shutdownRecord?.settled) {
|
|
11045
12198
|
shutdownRecord = null;
|
|
@@ -11052,18 +12205,22 @@ function createCopilotChannelsRuntime(options = {}) {
|
|
|
11052
12205
|
attachSession,
|
|
11053
12206
|
tools,
|
|
11054
12207
|
hooks,
|
|
12208
|
+
canvases,
|
|
11055
12209
|
stopAllEmitters,
|
|
11056
12210
|
stopAllEmittersAndWait,
|
|
11057
12211
|
handleSessionShutdown,
|
|
11058
12212
|
appendStreamMessage: runtimeService.session.appendStreamMessage,
|
|
11059
12213
|
getTools: () => gateway.isRunning() ? gateway.getAllTools(tools) : tools,
|
|
12214
|
+
getCanvases: () => canvases,
|
|
11060
12215
|
DEFAULT_STREAM
|
|
11061
12216
|
};
|
|
11062
12217
|
}
|
|
11063
12218
|
|
|
11064
12219
|
// src/extension.mjs
|
|
12220
|
+
var diagnostics = globalThis.__tapDiagnostics ??= createDiagnosticsStore();
|
|
11065
12221
|
function tapLog(msg) {
|
|
11066
12222
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
12223
|
+
diagnostics.log("extension", msg);
|
|
11067
12224
|
process.stderr.write(`[tap ${ts}] ${msg}
|
|
11068
12225
|
`);
|
|
11069
12226
|
}
|
|
@@ -11071,7 +12228,8 @@ tapLog(`extension.mjs loading \u2014 pid=${process.pid} cwd=${process.cwd()} SES
|
|
|
11071
12228
|
var isResume = Boolean(globalThis.__tapRuntime);
|
|
11072
12229
|
tapLog(`runtime ${isResume ? "resuming (cached)" : "creating (fresh)"}`);
|
|
11073
12230
|
var runtime = globalThis.__tapRuntime ??= createCopilotChannelsRuntime({
|
|
11074
|
-
cwd: process.cwd()
|
|
12231
|
+
cwd: process.cwd(),
|
|
12232
|
+
diagnostics
|
|
11075
12233
|
});
|
|
11076
12234
|
tapLog("runtime ready");
|
|
11077
12235
|
tapLog("calling joinSession\u2026");
|
|
@@ -11079,6 +12237,7 @@ var session;
|
|
|
11079
12237
|
try {
|
|
11080
12238
|
session = await joinSession({
|
|
11081
12239
|
tools: runtime.getTools(),
|
|
12240
|
+
canvases: runtime.getCanvases(),
|
|
11082
12241
|
hooks: runtime.hooks
|
|
11083
12242
|
});
|
|
11084
12243
|
tapLog(`joinSession OK \u2014 session.id=${session.id ?? "(none)"}`);
|