copilot-tap-extension 2.0.5 → 2.0.6

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.
@@ -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 URL) {
2788
+ if (address instanceof URL2) {
2789
2789
  parsedUrl = address;
2790
2790
  } else {
2791
2791
  try {
2792
- parsedUrl = new URL(address);
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 URL(location, address);
2929
+ addr = new URL2(location, address);
2930
2930
  } catch (e) {
2931
2931
  const err = new SyntaxError(`Invalid URL: ${location}`);
2932
2932
  emitErrorAndClose(websocket, err);
@@ -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(`Failed to start provider gateway on port ${GATEWAY_PORT}: ${err.message}`);
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,
@@ -10258,6 +10356,16 @@ function createSessionPort(initialSession = null) {
10258
10356
  }
10259
10357
  return session2.sendAndWait({ prompt });
10260
10358
  }
10359
+ async function openCanvas(params) {
10360
+ if (!session2) {
10361
+ throw new LifecycleError("Session is not attached; cannot open canvas.");
10362
+ }
10363
+ const canvasApi = session2.rpc?.canvas;
10364
+ if (!canvasApi || typeof canvasApi.open !== "function") {
10365
+ throw new LifecycleError("Canvas renderer API is not available in this Copilot session.");
10366
+ }
10367
+ return canvasApi.open(params);
10368
+ }
10261
10369
  function registerTools(tools) {
10262
10370
  if (!session2) return;
10263
10371
  try {
@@ -10293,6 +10401,7 @@ function createSessionPort(initialSession = null) {
10293
10401
  log,
10294
10402
  send,
10295
10403
  sendAndWait,
10404
+ openCanvas,
10296
10405
  registerTools,
10297
10406
  reloadExtension
10298
10407
  };
@@ -10536,7 +10645,23 @@ function createNotificationDispatcher({
10536
10645
  }
10537
10646
  return { cleared, generation };
10538
10647
  }
10539
- return { enqueue, clear, dispose: clear };
10648
+ function snapshot(options = {}) {
10649
+ const limit = Math.max(0, Math.min(Number(options.limit ?? 20) || 20, queueLimit));
10650
+ return {
10651
+ queueSize: queue.length,
10652
+ maxQueueSize: queueLimit,
10653
+ inFlight,
10654
+ retryScheduled: retryTimer !== null,
10655
+ generation,
10656
+ queued: queue.slice(0, limit).map((entry) => ({
10657
+ channel: entry.channel,
10658
+ monitorName: entry.monitorName,
10659
+ stream: entry.stream,
10660
+ text: String(entry.text ?? "").slice(0, 500)
10661
+ }))
10662
+ };
10663
+ }
10664
+ return { enqueue, clear, dispose: clear, snapshot };
10540
10665
  }
10541
10666
 
10542
10667
  // src/services/runtime-subsystems.mjs
@@ -10732,8 +10857,263 @@ function createStreamService(deps) {
10732
10857
  };
10733
10858
  }
10734
10859
 
10860
+ // src/diagnostics/store.mjs
10861
+ var DEFAULT_MAX_LOGS = 300;
10862
+ var DEFAULT_MAX_EVENTS = 300;
10863
+ var DEFAULT_MAX_RUNTIME_EVENTS = 300;
10864
+ var MAX_STRING_LENGTH = 1200;
10865
+ var MAX_COLLECTION_ITEMS = 40;
10866
+ var MAX_DEPTH = 4;
10867
+ var SECRET_KEY_PATTERN = /(?:token|secret|password|credential|authorization|api[-_]?key|reconnectToken|expectedToken)/i;
10868
+ function normalizePositiveInteger(value, fallback) {
10869
+ const number = Number(value);
10870
+ if (!Number.isFinite(number)) {
10871
+ return fallback;
10872
+ }
10873
+ return Math.max(0, Math.floor(number));
10874
+ }
10875
+ function createRingBuffer(maxEntries) {
10876
+ const entries = [];
10877
+ const limit = normalizePositiveInteger(maxEntries, 0);
10878
+ let total = 0;
10879
+ let dropped = 0;
10880
+ function append(entry) {
10881
+ total += 1;
10882
+ if (limit === 0) {
10883
+ dropped += 1;
10884
+ return null;
10885
+ }
10886
+ entries.push(entry);
10887
+ if (entries.length > limit) {
10888
+ const overflow = entries.length - limit;
10889
+ entries.splice(0, overflow);
10890
+ dropped += overflow;
10891
+ }
10892
+ return entry;
10893
+ }
10894
+ function snapshot(limitOverride) {
10895
+ const requested = normalizePositiveInteger(limitOverride, entries.length);
10896
+ return entries.slice(Math.max(0, entries.length - requested)).map((entry) => safeClone(entry));
10897
+ }
10898
+ function stats() {
10899
+ return {
10900
+ retained: entries.length,
10901
+ total,
10902
+ dropped,
10903
+ capacity: limit
10904
+ };
10905
+ }
10906
+ return { append, snapshot, stats };
10907
+ }
10908
+ function truncateString(value, maxLength = MAX_STRING_LENGTH) {
10909
+ const text = String(value ?? "");
10910
+ if (text.length <= maxLength) {
10911
+ return text;
10912
+ }
10913
+ return `${text.slice(0, maxLength)}... (${text.length - maxLength} chars truncated)`;
10914
+ }
10915
+ function safeClone(value, options = {}, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
10916
+ const maxDepth = normalizePositiveInteger(options.maxDepth, MAX_DEPTH);
10917
+ const maxStringLength = normalizePositiveInteger(options.maxStringLength, MAX_STRING_LENGTH);
10918
+ const maxCollectionItems = normalizePositiveInteger(options.maxCollectionItems, MAX_COLLECTION_ITEMS);
10919
+ if (value === null || value === void 0) {
10920
+ return value ?? null;
10921
+ }
10922
+ if (typeof value === "string") {
10923
+ return truncateString(value, maxStringLength);
10924
+ }
10925
+ if (typeof value === "number" || typeof value === "boolean") {
10926
+ return value;
10927
+ }
10928
+ if (typeof value === "bigint") {
10929
+ return String(value);
10930
+ }
10931
+ if (typeof value === "function" || typeof value === "symbol") {
10932
+ return `[${typeof value}]`;
10933
+ }
10934
+ if (value instanceof Error) {
10935
+ return {
10936
+ name: value.name,
10937
+ message: truncateString(value.message, maxStringLength),
10938
+ stack: truncateString(value.stack ?? "", maxStringLength)
10939
+ };
10940
+ }
10941
+ if (depth >= maxDepth) {
10942
+ return Array.isArray(value) ? `[Array(${value.length})]` : "[Object]";
10943
+ }
10944
+ if (typeof value !== "object") {
10945
+ return String(value);
10946
+ }
10947
+ if (seen.has(value)) {
10948
+ return "[Circular]";
10949
+ }
10950
+ seen.add(value);
10951
+ if (Array.isArray(value)) {
10952
+ const slice = value.slice(0, maxCollectionItems).map((item) => safeClone(item, options, depth + 1, seen));
10953
+ if (value.length > maxCollectionItems) {
10954
+ slice.push(`... ${value.length - maxCollectionItems} more items`);
10955
+ }
10956
+ return slice;
10957
+ }
10958
+ const output = {};
10959
+ const entries = Object.entries(value).slice(0, maxCollectionItems);
10960
+ for (const [key, item] of entries) {
10961
+ output[key] = SECRET_KEY_PATTERN.test(key) ? "[redacted]" : safeClone(item, options, depth + 1, seen);
10962
+ }
10963
+ const remaining = Object.keys(value).length - entries.length;
10964
+ if (remaining > 0) {
10965
+ output.__truncatedKeys = remaining;
10966
+ }
10967
+ return output;
10968
+ }
10969
+ function normalizeLevel(value) {
10970
+ const level = String(value ?? "info").trim().toLowerCase();
10971
+ if (level === "warning" || level === "warn") return "warning";
10972
+ if (level === "error") return "error";
10973
+ if (level === "debug") return "debug";
10974
+ return "info";
10975
+ }
10976
+ function createId(prefix, count) {
10977
+ return `${prefix}-${count.toString(36)}`;
10978
+ }
10979
+ function summarizeSessionEvent(event) {
10980
+ const data = event?.data && typeof event.data === "object" ? event.data : {};
10981
+ return {
10982
+ id: event?.id ?? null,
10983
+ timestamp: event?.timestamp ?? nowIso(),
10984
+ type: String(event?.type ?? "unknown"),
10985
+ ephemeral: event?.ephemeral === true,
10986
+ agentId: event?.agentId ?? null,
10987
+ dataKeys: Object.keys(data),
10988
+ data: safeClone(data, {
10989
+ maxDepth: 3,
10990
+ maxStringLength: 700,
10991
+ maxCollectionItems: 24
10992
+ })
10993
+ };
10994
+ }
10995
+ function createDiagnosticsStore(options = {}) {
10996
+ const logs = createRingBuffer(options.maxLogs ?? DEFAULT_MAX_LOGS);
10997
+ const sessionEvents = createRingBuffer(options.maxSessionEvents ?? DEFAULT_MAX_EVENTS);
10998
+ const runtimeEvents = createRingBuffer(options.maxRuntimeEvents ?? DEFAULT_MAX_RUNTIME_EVENTS);
10999
+ const sessionEventCounts = /* @__PURE__ */ new Map();
11000
+ let logCount = 0;
11001
+ let runtimeEventCount = 0;
11002
+ let sessionEventCount = 0;
11003
+ let cleanupSessionListener = () => {
11004
+ };
11005
+ function recordLog(source, message, options2 = {}) {
11006
+ logCount += 1;
11007
+ return logs.append({
11008
+ id: createId("log", logCount),
11009
+ timestamp: nowIso(),
11010
+ source: String(source ?? "tap"),
11011
+ level: normalizeLevel(options2.level),
11012
+ message: truncateString(message, options2.maxStringLength ?? MAX_STRING_LENGTH),
11013
+ metadata: safeClone(options2.metadata ?? null, {
11014
+ maxDepth: 3,
11015
+ maxStringLength: 700,
11016
+ maxCollectionItems: 20
11017
+ })
11018
+ });
11019
+ }
11020
+ function recordRuntimeEvent(type, message, metadata = {}) {
11021
+ runtimeEventCount += 1;
11022
+ return runtimeEvents.append({
11023
+ id: createId("evt", runtimeEventCount),
11024
+ timestamp: nowIso(),
11025
+ type: String(type ?? "runtime"),
11026
+ message: truncateString(message, MAX_STRING_LENGTH),
11027
+ metadata: safeClone(metadata, {
11028
+ maxDepth: 3,
11029
+ maxStringLength: 700,
11030
+ maxCollectionItems: 20
11031
+ })
11032
+ });
11033
+ }
11034
+ function recordSessionEvent(event) {
11035
+ sessionEventCount += 1;
11036
+ const type = String(event?.type ?? "unknown");
11037
+ sessionEventCounts.set(type, (sessionEventCounts.get(type) ?? 0) + 1);
11038
+ return sessionEvents.append({
11039
+ sequence: sessionEventCount,
11040
+ ...summarizeSessionEvent(event)
11041
+ });
11042
+ }
11043
+ function detachSession() {
11044
+ try {
11045
+ cleanupSessionListener();
11046
+ } catch {
11047
+ }
11048
+ cleanupSessionListener = () => {
11049
+ };
11050
+ }
11051
+ function attachSession(session2) {
11052
+ detachSession();
11053
+ if (!session2 || typeof session2.on !== "function") {
11054
+ recordRuntimeEvent("session-events", "Session event capture unavailable; no session listener attached.");
11055
+ return;
11056
+ }
11057
+ try {
11058
+ const unsubscribe = session2.on((event) => {
11059
+ recordSessionEvent(event);
11060
+ });
11061
+ cleanupSessionListener = typeof unsubscribe === "function" ? unsubscribe : () => {
11062
+ };
11063
+ recordRuntimeEvent("session-events", "Session event capture attached.");
11064
+ } catch (error) {
11065
+ recordLog("diagnostics", "Failed to attach session event capture.", {
11066
+ level: "warning",
11067
+ metadata: { error }
11068
+ });
11069
+ }
11070
+ }
11071
+ function snapshot(options2 = {}) {
11072
+ return {
11073
+ generatedAt: nowIso(),
11074
+ logs: logs.snapshot(options2.logLimit ?? 140),
11075
+ runtimeEvents: runtimeEvents.snapshot(options2.runtimeEventLimit ?? 140),
11076
+ sessionEvents: sessionEvents.snapshot(options2.sessionEventLimit ?? 140),
11077
+ sessionEventCounts: Object.fromEntries([...sessionEventCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
11078
+ stats: {
11079
+ logs: logs.stats(),
11080
+ runtimeEvents: runtimeEvents.stats(),
11081
+ sessionEvents: sessionEvents.stats()
11082
+ }
11083
+ };
11084
+ }
11085
+ return {
11086
+ log: recordLog,
11087
+ event: recordRuntimeEvent,
11088
+ attachSession,
11089
+ detachSession,
11090
+ snapshot
11091
+ };
11092
+ }
11093
+
11094
+ // src/canvas/consts.mjs
11095
+ var TAP_DIAGNOSTICS_CANVAS_ID = "tap-diagnostics";
11096
+
10735
11097
  // src/services/tap-runtime-service.mjs
11098
+ function trimStreamEntries(stream, limit) {
11099
+ const entryLimit = Math.max(0, Math.floor(Number(limit ?? 80) || 80));
11100
+ return {
11101
+ ...stream,
11102
+ entries: Array.isArray(stream.entries) ? stream.entries.slice(Math.max(0, stream.entries.length - entryLimit)) : []
11103
+ };
11104
+ }
11105
+ function projectToolForDiagnostics(tool) {
11106
+ return {
11107
+ name: tool.name,
11108
+ description: tool.description ?? "",
11109
+ providerId: tool.providerId ?? null,
11110
+ providerName: tool.providerName ?? null,
11111
+ skipPermission: tool.skipPermission === true,
11112
+ overridesBuiltInTool: tool.overridesBuiltInTool === true
11113
+ };
11114
+ }
10736
11115
  function createTapRuntimeService(options = {}) {
11116
+ const diagnosticsStore = options.diagnostics ?? createDiagnosticsStore();
10737
11117
  const {
10738
11118
  streams,
10739
11119
  configStore,
@@ -10780,6 +11160,7 @@ function createTapRuntimeService(options = {}) {
10780
11160
  sessionContext.attachSession(session2);
10781
11161
  sessionPort.attach(session2);
10782
11162
  sessionActivityBridge.attach(session2);
11163
+ diagnosticsStore.attachSession(session2);
10783
11164
  }
10784
11165
  function clearNotificationsForLifecycle(options2 = {}) {
10785
11166
  if (options2.clearNotifications === true && typeof notifications.clear === "function") {
@@ -10852,6 +11233,7 @@ function createTapRuntimeService(options = {}) {
10852
11233
  const providerCapabilities = {
10853
11234
  getSessionInfo,
10854
11235
  log: (msg) => {
11236
+ diagnosticsStore.log("gateway", msg);
10855
11237
  process.stderr.write(`[tap-gateway] ${msg}
10856
11238
  `);
10857
11239
  void sessionPort.log(msg);
@@ -10862,14 +11244,53 @@ function createTapRuntimeService(options = {}) {
10862
11244
  void sessionPort.reloadExtension();
10863
11245
  }
10864
11246
  };
11247
+ function getDiagnosticsSnapshot(options2 = {}, extra = {}) {
11248
+ const allTools = Array.isArray(extra.tools) ? extra.tools.map(projectToolForDiagnostics) : [];
11249
+ return {
11250
+ generatedAt: nowIso(),
11251
+ session: getSessionInfo(),
11252
+ baseCwd: sessionContext.getBaseCwd(),
11253
+ process: {
11254
+ pid: process.pid,
11255
+ platform: process.platform,
11256
+ uptimeSeconds: Math.round(process.uptime())
11257
+ },
11258
+ streams: streamCapabilities.listStreams().map((stream) => trimStreamEntries(stream, options2.streamEntryLimit)),
11259
+ emitters: emitterCapabilities.listEmitters(),
11260
+ gateway: extra.gateway ?? null,
11261
+ tools: allTools,
11262
+ notifications: typeof notifications.snapshot === "function" ? notifications.snapshot({ limit: options2.notificationLimit ?? 20 }) : null,
11263
+ diagnostics: diagnosticsStore.snapshot(options2)
11264
+ };
11265
+ }
11266
+ const diagnosticsCapabilities = {
11267
+ log: (source, message, options2 = {}) => diagnosticsStore.log(source, message, options2),
11268
+ event: (type, message, metadata = {}) => diagnosticsStore.event(type, message, metadata),
11269
+ snapshot: getDiagnosticsSnapshot,
11270
+ openCanvas: async (input = {}) => {
11271
+ const instanceId = String(input.instanceId ?? TAP_DIAGNOSTICS_CANVAS_ID).trim() || TAP_DIAGNOSTICS_CANVAS_ID;
11272
+ const limit = Number(input.limit);
11273
+ const canvasInput = Number.isFinite(limit) ? { limit: Math.max(10, Math.min(300, Math.floor(limit))) } : {};
11274
+ diagnosticsStore.event("canvas.open.requested", "Opening tap diagnostics canvas.", { instanceId, canvasInput });
11275
+ return sessionPort.openCanvas({
11276
+ canvasId: TAP_DIAGNOSTICS_CANVAS_ID,
11277
+ instanceId,
11278
+ input: canvasInput
11279
+ });
11280
+ },
11281
+ attachSession: diagnosticsStore.attachSession,
11282
+ detachSession: diagnosticsStore.detachSession
11283
+ };
10865
11284
  return {
10866
11285
  tools: {
10867
11286
  streams: streamCapabilities,
10868
- emitters: emitterCapabilities
11287
+ emitters: emitterCapabilities,
11288
+ diagnostics: diagnosticsCapabilities
10869
11289
  },
10870
11290
  hooks: hookCapabilities,
10871
11291
  session: sessionCapabilities,
10872
11292
  provider: providerCapabilities,
11293
+ diagnostics: diagnosticsCapabilities,
10873
11294
  getBaseCwd: sessionContext.getBaseCwd,
10874
11295
  getSessionInfo,
10875
11296
  attachSession,
@@ -10892,6 +11313,725 @@ function createTapRuntimeService(options = {}) {
10892
11313
  };
10893
11314
  }
10894
11315
 
11316
+ // src/canvas/diagnostics-canvas.mjs
11317
+ import { createServer } from "node:http";
11318
+ import { createCanvas } from "@github/copilot-sdk/extension";
11319
+ var DEFAULT_REFRESH_MS = 1200;
11320
+ function jsonResponse(res, status, data) {
11321
+ const body = JSON.stringify(data, null, 2);
11322
+ res.writeHead(status, {
11323
+ "Content-Type": "application/json; charset=utf-8",
11324
+ "Cache-Control": "no-store",
11325
+ "X-Content-Type-Options": "nosniff"
11326
+ });
11327
+ res.end(body);
11328
+ }
11329
+ function textResponse(res, status, body, contentType = "text/plain; charset=utf-8") {
11330
+ res.writeHead(status, {
11331
+ "Content-Type": contentType,
11332
+ "Cache-Control": "no-store",
11333
+ "X-Content-Type-Options": "nosniff"
11334
+ });
11335
+ res.end(body);
11336
+ }
11337
+ function sanitizeSnapshotOptions(input = {}) {
11338
+ const source = input && typeof input === "object" ? input : {};
11339
+ const limit = Number(source.limit);
11340
+ return {
11341
+ streamEntryLimit: Number.isFinite(limit) ? Math.max(10, Math.min(200, Math.floor(limit))) : 80,
11342
+ logLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
11343
+ sessionEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160,
11344
+ runtimeEventLimit: Number.isFinite(limit) ? Math.max(20, Math.min(300, Math.floor(limit))) : 160
11345
+ };
11346
+ }
11347
+ function summarizeSnapshot(snapshot) {
11348
+ const runningEmitters = Array.isArray(snapshot?.emitters?.running) ? snapshot.emitters.running.length : 0;
11349
+ const configuredEmitters = Array.isArray(snapshot?.emitters?.configured) ? snapshot.emitters.configured.length : 0;
11350
+ const streams = Array.isArray(snapshot?.streams) ? snapshot.streams.length : 0;
11351
+ const providers = Array.isArray(snapshot?.gateway?.providers) ? snapshot.gateway.providers.length : 0;
11352
+ const logs = snapshot?.diagnostics?.stats?.logs?.retained ?? 0;
11353
+ const sessionEvents = snapshot?.diagnostics?.stats?.sessionEvents?.retained ?? 0;
11354
+ return { streams, runningEmitters, configuredEmitters, providers, logs, sessionEvents };
11355
+ }
11356
+ function createHtml() {
11357
+ return `<!doctype html>
11358
+ <html lang="en">
11359
+ <head>
11360
+ <meta charset="utf-8" />
11361
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
11362
+ <title>tap diagnostics</title>
11363
+ <style>
11364
+ :root {
11365
+ color-scheme: light;
11366
+ --paper: oklch(96% 0.018 86);
11367
+ --paper-2: oklch(91% 0.025 82);
11368
+ --ink: oklch(18% 0.026 70);
11369
+ --muted: oklch(43% 0.04 73);
11370
+ --line: oklch(72% 0.052 76);
11371
+ --amber: oklch(70% 0.15 72);
11372
+ --amber-dark: oklch(42% 0.12 67);
11373
+ --red: oklch(55% 0.18 30);
11374
+ --green: oklch(55% 0.13 145);
11375
+ --blue: oklch(48% 0.1 245);
11376
+ --surface: oklch(99% 0.012 88);
11377
+ --shadow: 0 24px 80px oklch(28% 0.04 70 / 0.14);
11378
+ --font-display: "Bahnschrift", "Aptos Display", "Segoe UI Variable Display", sans-serif;
11379
+ --font-body: "Aptos", "Segoe UI Variable Text", sans-serif;
11380
+ --font-mono: "Cascadia Code", "Consolas", monospace;
11381
+ }
11382
+
11383
+ * { box-sizing: border-box; }
11384
+ html { min-height: 100%; background: var(--paper); color: var(--ink); }
11385
+ body {
11386
+ margin: 0;
11387
+ min-height: 100vh;
11388
+ font-family: var(--font-body);
11389
+ background:
11390
+ linear-gradient(90deg, oklch(34% 0.08 68 / 0.08) 1px, transparent 1px) 0 0 / 42px 42px,
11391
+ linear-gradient(0deg, oklch(34% 0.08 68 / 0.055) 1px, transparent 1px) 0 0 / 42px 42px,
11392
+ radial-gradient(circle at 8% 12%, oklch(76% 0.13 74 / 0.35), transparent 28rem),
11393
+ var(--paper);
11394
+ }
11395
+
11396
+ button, input { font: inherit; }
11397
+ button:focus-visible, input:focus-visible {
11398
+ outline: 3px solid var(--amber-dark);
11399
+ outline-offset: 3px;
11400
+ }
11401
+
11402
+ .shell {
11403
+ width: min(1540px, calc(100vw - clamp(18px, 4vw, 64px)));
11404
+ margin: 0 auto;
11405
+ padding: clamp(22px, 4vw, 54px) 0;
11406
+ }
11407
+
11408
+ .masthead {
11409
+ display: grid;
11410
+ grid-template-columns: minmax(0, 1fr);
11411
+ gap: 22px;
11412
+ align-items: end;
11413
+ border-bottom: 3px solid var(--ink);
11414
+ padding-bottom: clamp(18px, 3vw, 34px);
11415
+ }
11416
+
11417
+ .mark {
11418
+ width: 72px;
11419
+ aspect-ratio: 1;
11420
+ display: grid;
11421
+ place-items: center;
11422
+ border: 3px solid var(--ink);
11423
+ background: var(--amber);
11424
+ box-shadow: 8px 8px 0 var(--ink);
11425
+ font-family: var(--font-display);
11426
+ font-size: 44px;
11427
+ line-height: 1;
11428
+ }
11429
+
11430
+ .title-wrap {
11431
+ display: grid;
11432
+ grid-template-columns: auto minmax(0, 1fr);
11433
+ gap: clamp(16px, 3vw, 28px);
11434
+ align-items: center;
11435
+ }
11436
+
11437
+ h1 {
11438
+ margin: 0;
11439
+ font-family: var(--font-display);
11440
+ font-size: clamp(2.4rem, 7vw, 6.8rem);
11441
+ line-height: 0.82;
11442
+ letter-spacing: -0.08em;
11443
+ text-transform: uppercase;
11444
+ max-width: 11ch;
11445
+ }
11446
+
11447
+ .dek {
11448
+ margin: 12px 0 0;
11449
+ max-width: 78ch;
11450
+ color: var(--muted);
11451
+ font-size: clamp(0.98rem, 1.8vw, 1.18rem);
11452
+ line-height: 1.45;
11453
+ }
11454
+
11455
+ .controls {
11456
+ display: flex;
11457
+ flex-wrap: wrap;
11458
+ gap: 10px;
11459
+ align-items: center;
11460
+ justify-content: space-between;
11461
+ }
11462
+
11463
+ .filter-row {
11464
+ display: flex;
11465
+ gap: 8px;
11466
+ flex-wrap: wrap;
11467
+ }
11468
+
11469
+ .chip, .button {
11470
+ border: 2px solid var(--ink);
11471
+ background: var(--surface);
11472
+ color: var(--ink);
11473
+ min-height: 40px;
11474
+ padding: 8px 13px;
11475
+ box-shadow: 3px 3px 0 var(--ink);
11476
+ cursor: pointer;
11477
+ transition: transform 140ms ease, box-shadow 140ms ease, background 140ms ease;
11478
+ }
11479
+
11480
+ .chip[aria-pressed="true"], .button.primary {
11481
+ background: var(--ink);
11482
+ color: var(--paper);
11483
+ }
11484
+
11485
+ @media (hover: hover) {
11486
+ .chip:hover, .button:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--ink); }
11487
+ }
11488
+
11489
+ .search {
11490
+ display: flex;
11491
+ align-items: center;
11492
+ gap: 10px;
11493
+ min-width: min(100%, 380px);
11494
+ border: 2px solid var(--ink);
11495
+ background: var(--surface);
11496
+ box-shadow: 3px 3px 0 var(--ink);
11497
+ padding: 8px 12px;
11498
+ }
11499
+
11500
+ .search input {
11501
+ width: 100%;
11502
+ border: 0;
11503
+ background: transparent;
11504
+ color: var(--ink);
11505
+ outline: 0;
11506
+ }
11507
+
11508
+ .dashboard {
11509
+ display: grid;
11510
+ grid-template-columns: minmax(0, 1fr);
11511
+ gap: clamp(18px, 3vw, 34px);
11512
+ margin-top: clamp(22px, 4vw, 48px);
11513
+ }
11514
+
11515
+ .metrics {
11516
+ display: grid;
11517
+ grid-template-columns: repeat(2, minmax(0, 1fr));
11518
+ gap: 12px;
11519
+ }
11520
+
11521
+ .metric {
11522
+ min-height: 118px;
11523
+ border: 2px solid var(--ink);
11524
+ background: var(--surface);
11525
+ box-shadow: var(--shadow);
11526
+ padding: 16px;
11527
+ display: flex;
11528
+ flex-direction: column;
11529
+ justify-content: space-between;
11530
+ }
11531
+
11532
+ .metric b {
11533
+ font-family: var(--font-display);
11534
+ font-size: clamp(2rem, 5vw, 4.2rem);
11535
+ line-height: 0.9;
11536
+ letter-spacing: -0.05em;
11537
+ }
11538
+
11539
+ .metric span {
11540
+ color: var(--muted);
11541
+ text-transform: uppercase;
11542
+ font-size: 0.72rem;
11543
+ letter-spacing: 0.14em;
11544
+ font-weight: 800;
11545
+ }
11546
+
11547
+ .panel {
11548
+ border: 2px solid var(--ink);
11549
+ background: oklch(98% 0.014 88 / 0.96);
11550
+ box-shadow: 8px 8px 0 oklch(18% 0.026 70 / 0.22);
11551
+ min-width: 0;
11552
+ }
11553
+
11554
+ .panel-head {
11555
+ display: flex;
11556
+ align-items: baseline;
11557
+ justify-content: space-between;
11558
+ gap: 12px;
11559
+ padding: 14px 16px;
11560
+ border-bottom: 2px solid var(--ink);
11561
+ background: var(--paper-2);
11562
+ }
11563
+
11564
+ .panel-head h2 {
11565
+ margin: 0;
11566
+ font-family: var(--font-display);
11567
+ font-size: clamp(1.2rem, 2vw, 1.7rem);
11568
+ letter-spacing: -0.03em;
11569
+ text-transform: uppercase;
11570
+ }
11571
+
11572
+ .panel-head small {
11573
+ color: var(--muted);
11574
+ font-family: var(--font-mono);
11575
+ font-size: 0.75rem;
11576
+ }
11577
+
11578
+ .panel-body {
11579
+ padding: 14px;
11580
+ max-height: min(62vh, 720px);
11581
+ overflow: auto;
11582
+ }
11583
+
11584
+ .stream-grid, .emitter-grid, .provider-grid {
11585
+ display: grid;
11586
+ gap: 12px;
11587
+ }
11588
+
11589
+ .record {
11590
+ border-left: 4px solid var(--line);
11591
+ background: color-mix(in oklch, var(--surface), var(--paper) 24%);
11592
+ padding: 11px 12px;
11593
+ }
11594
+
11595
+ .record strong {
11596
+ display: inline-flex;
11597
+ gap: 8px;
11598
+ align-items: baseline;
11599
+ font-family: var(--font-display);
11600
+ letter-spacing: -0.01em;
11601
+ }
11602
+
11603
+ .meta {
11604
+ color: var(--muted);
11605
+ font-family: var(--font-mono);
11606
+ font-size: 0.72rem;
11607
+ overflow-wrap: anywhere;
11608
+ }
11609
+
11610
+ .entry {
11611
+ margin-top: 8px;
11612
+ padding-top: 8px;
11613
+ border-top: 1px dashed var(--line);
11614
+ font-family: var(--font-mono);
11615
+ font-size: 0.78rem;
11616
+ line-height: 1.45;
11617
+ white-space: pre-wrap;
11618
+ overflow-wrap: anywhere;
11619
+ }
11620
+
11621
+ .timeline {
11622
+ display: grid;
11623
+ gap: 9px;
11624
+ }
11625
+
11626
+ .tick {
11627
+ display: grid;
11628
+ grid-template-columns: 90px minmax(0, 1fr);
11629
+ gap: 12px;
11630
+ border-bottom: 1px dashed var(--line);
11631
+ padding: 9px 0;
11632
+ }
11633
+
11634
+ .tick time {
11635
+ color: var(--muted);
11636
+ font-family: var(--font-mono);
11637
+ font-size: 0.72rem;
11638
+ }
11639
+
11640
+ .badge {
11641
+ display: inline-flex;
11642
+ align-items: center;
11643
+ gap: 6px;
11644
+ padding: 2px 7px;
11645
+ border: 1px solid var(--ink);
11646
+ background: var(--paper);
11647
+ font-family: var(--font-mono);
11648
+ font-size: 0.68rem;
11649
+ text-transform: uppercase;
11650
+ letter-spacing: 0.08em;
11651
+ }
11652
+
11653
+ .badge.error { background: color-mix(in oklch, var(--red), var(--paper) 72%); }
11654
+ .badge.warning { background: color-mix(in oklch, var(--amber), var(--paper) 54%); }
11655
+ .badge.ready, .badge.running, .badge.success { background: color-mix(in oklch, var(--green), var(--paper) 70%); }
11656
+ .badge.info, .badge.debug { background: color-mix(in oklch, var(--blue), var(--paper) 76%); }
11657
+
11658
+ .details {
11659
+ margin-top: 6px;
11660
+ color: var(--ink);
11661
+ font-family: var(--font-mono);
11662
+ font-size: 0.78rem;
11663
+ white-space: pre-wrap;
11664
+ overflow-wrap: anywhere;
11665
+ }
11666
+
11667
+ .empty {
11668
+ border: 2px dashed var(--line);
11669
+ padding: 18px;
11670
+ color: var(--muted);
11671
+ background: color-mix(in oklch, var(--surface), var(--paper) 45%);
11672
+ }
11673
+
11674
+ .footer {
11675
+ margin-top: 24px;
11676
+ color: var(--muted);
11677
+ font-size: 0.86rem;
11678
+ }
11679
+
11680
+ @media (min-width: 780px) {
11681
+ .masthead { grid-template-columns: minmax(0, 1fr) auto; }
11682
+ .dashboard { grid-template-columns: minmax(280px, 0.62fr) minmax(0, 1fr); align-items: start; }
11683
+ .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
11684
+ .timeline-panel { grid-column: 1 / -1; }
11685
+ }
11686
+
11687
+ @media (min-width: 1180px) {
11688
+ .dashboard { grid-template-columns: 360px minmax(0, 1fr) 420px; }
11689
+ .timeline-panel { grid-column: auto; }
11690
+ .metrics { grid-template-columns: 1fr; }
11691
+ }
11692
+
11693
+ @media (prefers-reduced-motion: reduce) {
11694
+ *, *::before, *::after {
11695
+ animation-duration: 0.01ms !important;
11696
+ animation-iteration-count: 1 !important;
11697
+ scroll-behavior: auto !important;
11698
+ transition-duration: 0.01ms !important;
11699
+ }
11700
+ }
11701
+ </style>
11702
+ </head>
11703
+ <body>
11704
+ <main class="shell">
11705
+ <header class="masthead">
11706
+ <div class="title-wrap">
11707
+ <div class="mark" aria-hidden="true">\u203B</div>
11708
+ <div>
11709
+ <h1>Tap flight recorder</h1>
11710
+ <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>
11711
+ </div>
11712
+ </div>
11713
+ <div class="controls" aria-label="Diagnostics controls">
11714
+ <div class="filter-row" role="group" aria-label="Timeline filter">
11715
+ <button class="chip" data-filter="all" aria-pressed="true">All</button>
11716
+ <button class="chip" data-filter="streams" aria-pressed="false">Streams</button>
11717
+ <button class="chip" data-filter="logs" aria-pressed="false">Logs</button>
11718
+ <button class="chip" data-filter="session" aria-pressed="false">Session</button>
11719
+ <button class="chip" data-filter="runtime" aria-pressed="false">Runtime</button>
11720
+ </div>
11721
+ <label class="search">
11722
+ <span aria-hidden="true">\u2315</span>
11723
+ <input id="search" type="search" placeholder="Filter evidence..." autocomplete="off" />
11724
+ </label>
11725
+ <button id="pause" class="button" type="button">Pause</button>
11726
+ <button id="refresh" class="button primary" type="button">Refresh</button>
11727
+ </div>
11728
+ </header>
11729
+
11730
+ <section class="dashboard" aria-live="polite">
11731
+ <aside>
11732
+ <div class="metrics" id="metrics"></div>
11733
+ <p class="footer" id="heartbeat">Waiting for first snapshot...</p>
11734
+ </aside>
11735
+
11736
+ <section class="panel">
11737
+ <div class="panel-head">
11738
+ <h2>Streams and emitters</h2>
11739
+ <small id="stream-count">0 streams</small>
11740
+ </div>
11741
+ <div class="panel-body">
11742
+ <div class="stream-grid" id="streams"></div>
11743
+ </div>
11744
+ </section>
11745
+
11746
+ <section class="panel">
11747
+ <div class="panel-head">
11748
+ <h2>Providers</h2>
11749
+ <small id="provider-count">0 providers</small>
11750
+ </div>
11751
+ <div class="panel-body">
11752
+ <div class="provider-grid" id="providers"></div>
11753
+ </div>
11754
+ </section>
11755
+
11756
+ <section class="panel timeline-panel">
11757
+ <div class="panel-head">
11758
+ <h2>Evidence timeline</h2>
11759
+ <small id="timeline-count">0 events</small>
11760
+ </div>
11761
+ <div class="panel-body">
11762
+ <div class="timeline" id="timeline"></div>
11763
+ </div>
11764
+ </section>
11765
+ </section>
11766
+ </main>
11767
+
11768
+ <script>
11769
+ const state = { snapshot: null, paused: false, filter: "all", query: "" };
11770
+ const el = (id) => document.getElementById(id);
11771
+ const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch]));
11772
+ const timeOnly = (value) => {
11773
+ const date = new Date(value);
11774
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], { hour12: false });
11775
+ };
11776
+ const compactJson = (value) => {
11777
+ if (value === null || value === undefined) return "";
11778
+ if (typeof value === "string") return value;
11779
+ try { return JSON.stringify(value, null, 2); } catch { return String(value); }
11780
+ };
11781
+
11782
+ function metric(label, value, tone = "") {
11783
+ return '<article class="metric ' + tone + '"><span>' + escapeHtml(label) + '</span><b>' + escapeHtml(value) + '</b></article>';
11784
+ }
11785
+
11786
+ function renderMetrics(snapshot) {
11787
+ const running = snapshot.emitters?.running?.length ?? 0;
11788
+ const configured = snapshot.emitters?.configured?.length ?? 0;
11789
+ const streams = snapshot.streams?.length ?? 0;
11790
+ const providers = snapshot.gateway?.providers?.length ?? 0;
11791
+ const queue = snapshot.notifications?.queueSize ?? 0;
11792
+ const sessionEvents = snapshot.diagnostics?.stats?.sessionEvents?.retained ?? 0;
11793
+ el("metrics").innerHTML = [
11794
+ metric("streams", streams),
11795
+ metric("running emitters", running),
11796
+ metric("providers", providers),
11797
+ metric("queued injections", queue),
11798
+ metric("configured emitters", configured),
11799
+ metric("session events", sessionEvents)
11800
+ ].join("");
11801
+ el("heartbeat").textContent = "Snapshot " + timeOnly(snapshot.generatedAt) + " | pid " + (snapshot.process?.pid ?? "?") + " | gateway " + (snapshot.gateway?.running ? "ready" : "stopped");
11802
+ }
11803
+
11804
+ function renderStreams(snapshot) {
11805
+ const streams = snapshot.streams ?? [];
11806
+ el("stream-count").textContent = streams.length + " streams";
11807
+ if (streams.length === 0) {
11808
+ el("streams").innerHTML = '<div class="empty">No streams are currently retained.</div>';
11809
+ return;
11810
+ }
11811
+ const emitterRows = [...(snapshot.emitters?.running ?? []), ...(snapshot.emitters?.configured ?? [])]
11812
+ .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>')
11813
+ .join("");
11814
+ const streamRows = streams.map((stream) => {
11815
+ 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("");
11816
+ 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>';
11817
+ }).join("");
11818
+ 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>' : "");
11819
+ }
11820
+
11821
+ function renderProviders(snapshot) {
11822
+ const gateway = snapshot.gateway ?? {};
11823
+ const providers = gateway.providers ?? [];
11824
+ el("provider-count").textContent = providers.length + " providers";
11825
+ 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>';
11826
+ if (providers.length === 0) {
11827
+ el("providers").innerHTML = gatewayRecord + '<div class="empty">No external providers are bound. The canvas still shows tap-native streams and emitters.</div>';
11828
+ return;
11829
+ }
11830
+ const rows = providers.map((provider) => {
11831
+ const tools = (provider.tools ?? []).map((tool) => tool.name).join(", ") || "no tools";
11832
+ 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>';
11833
+ }).join("");
11834
+ el("providers").innerHTML = gatewayRecord + rows;
11835
+ }
11836
+
11837
+ function collectTimeline(snapshot) {
11838
+ const items = [];
11839
+ for (const stream of snapshot.streams ?? []) {
11840
+ for (const entry of stream.entries ?? []) {
11841
+ 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 });
11842
+ }
11843
+ }
11844
+ for (const log of snapshot.diagnostics?.logs ?? []) {
11845
+ items.push({ group: "logs", timestamp: log.timestamp, source: log.source, level: log.level, message: log.message, detail: compactJson(log.metadata) });
11846
+ }
11847
+ for (const event of snapshot.diagnostics?.sessionEvents ?? []) {
11848
+ items.push({ group: "session", timestamp: event.timestamp, source: "session", level: "debug", message: event.type, detail: compactJson(event.data) });
11849
+ }
11850
+ for (const event of snapshot.diagnostics?.runtimeEvents ?? []) {
11851
+ items.push({ group: "runtime", timestamp: event.timestamp, source: event.type, level: "info", message: event.message, detail: compactJson(event.metadata) });
11852
+ }
11853
+ return items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
11854
+ }
11855
+
11856
+ function renderTimeline(snapshot) {
11857
+ const query = state.query.trim().toLowerCase();
11858
+ const items = collectTimeline(snapshot).filter((item) => {
11859
+ if (state.filter !== "all" && item.group !== state.filter) return false;
11860
+ if (!query) return true;
11861
+ return [item.source, item.level, item.message, item.detail].join(" ").toLowerCase().includes(query);
11862
+ }).slice(0, 260);
11863
+ el("timeline-count").textContent = items.length + " visible";
11864
+ if (items.length === 0) {
11865
+ el("timeline").innerHTML = '<div class="empty">No evidence matches the current filter.</div>';
11866
+ return;
11867
+ }
11868
+ 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("");
11869
+ }
11870
+
11871
+ function render(snapshot) {
11872
+ state.snapshot = snapshot;
11873
+ renderMetrics(snapshot);
11874
+ renderStreams(snapshot);
11875
+ renderProviders(snapshot);
11876
+ renderTimeline(snapshot);
11877
+ }
11878
+
11879
+ async function refresh() {
11880
+ if (state.paused) return;
11881
+ const response = await fetch("/api/snapshot", { cache: "no-store" });
11882
+ if (response.ok) render(await response.json());
11883
+ }
11884
+
11885
+ function connectEvents() {
11886
+ if (!("EventSource" in window)) {
11887
+ setInterval(refresh, 1800);
11888
+ refresh();
11889
+ return;
11890
+ }
11891
+ const source = new EventSource("/events");
11892
+ source.addEventListener("snapshot", (event) => {
11893
+ if (!state.paused) render(JSON.parse(event.data));
11894
+ });
11895
+ source.onerror = () => {
11896
+ setTimeout(refresh, 2000);
11897
+ };
11898
+ }
11899
+
11900
+ document.querySelectorAll("[data-filter]").forEach((button) => {
11901
+ button.addEventListener("click", () => {
11902
+ state.filter = button.dataset.filter;
11903
+ document.querySelectorAll("[data-filter]").forEach((item) => item.setAttribute("aria-pressed", String(item === button)));
11904
+ if (state.snapshot) renderTimeline(state.snapshot);
11905
+ });
11906
+ });
11907
+ el("search").addEventListener("input", (event) => {
11908
+ state.query = event.target.value;
11909
+ if (state.snapshot) renderTimeline(state.snapshot);
11910
+ });
11911
+ el("pause").addEventListener("click", () => {
11912
+ state.paused = !state.paused;
11913
+ el("pause").textContent = state.paused ? "Resume" : "Pause";
11914
+ if (!state.paused) refresh();
11915
+ });
11916
+ el("refresh").addEventListener("click", () => refresh());
11917
+ connectEvents();
11918
+ </script>
11919
+ </body>
11920
+ </html>`;
11921
+ }
11922
+ function createTapDiagnosticsCanvas({ getSnapshot, diagnostics: diagnostics2 } = {}) {
11923
+ const instances = /* @__PURE__ */ new Map();
11924
+ function snapshot(options = {}) {
11925
+ return typeof getSnapshot === "function" ? getSnapshot(sanitizeSnapshotOptions(options)) : { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), error: "No diagnostics snapshot provider configured." };
11926
+ }
11927
+ function log(message, level = "info", metadata = {}) {
11928
+ diagnostics2?.log?.("canvas", message, { level, metadata });
11929
+ }
11930
+ async function startServer(instanceId) {
11931
+ const server = createServer((req, res) => {
11932
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
11933
+ if (url.pathname === "/") {
11934
+ textResponse(res, 200, createHtml(), "text/html; charset=utf-8");
11935
+ return;
11936
+ }
11937
+ if (url.pathname === "/api/snapshot") {
11938
+ jsonResponse(res, 200, snapshot({ limit: url.searchParams.get("limit") }));
11939
+ return;
11940
+ }
11941
+ if (url.pathname === "/events") {
11942
+ res.writeHead(200, {
11943
+ "Content-Type": "text/event-stream; charset=utf-8",
11944
+ "Cache-Control": "no-store",
11945
+ "Connection": "keep-alive",
11946
+ "X-Accel-Buffering": "no"
11947
+ });
11948
+ const send = () => {
11949
+ res.write(`event: snapshot
11950
+ data: ${JSON.stringify(snapshot())}
11951
+
11952
+ `);
11953
+ };
11954
+ send();
11955
+ const interval = setInterval(send, DEFAULT_REFRESH_MS);
11956
+ req.on("close", () => clearInterval(interval));
11957
+ return;
11958
+ }
11959
+ jsonResponse(res, 404, { error: "not_found" });
11960
+ });
11961
+ await new Promise((resolve, reject) => {
11962
+ server.once("error", reject);
11963
+ server.listen(0, "127.0.0.1", resolve);
11964
+ });
11965
+ const address = server.address();
11966
+ const port = typeof address === "object" && address ? address.port : 0;
11967
+ const entry = { server, url: `http://127.0.0.1:${port}/` };
11968
+ instances.set(instanceId, entry);
11969
+ log(`Diagnostics canvas server started for instance '${instanceId}'.`, "info", { url: entry.url });
11970
+ return entry;
11971
+ }
11972
+ async function stopServer(instanceId) {
11973
+ const entry = instances.get(instanceId);
11974
+ if (!entry) {
11975
+ return;
11976
+ }
11977
+ instances.delete(instanceId);
11978
+ await new Promise((resolve) => entry.server.close(() => resolve()));
11979
+ log(`Diagnostics canvas server stopped for instance '${instanceId}'.`);
11980
+ }
11981
+ return createCanvas({
11982
+ id: TAP_DIAGNOSTICS_CANVAS_ID,
11983
+ displayName: "Tap diagnostics",
11984
+ description: "Live tap flight recorder for streams, emitters, provider gateway state, logs, and session events.",
11985
+ inputSchema: {
11986
+ type: "object",
11987
+ properties: {
11988
+ limit: { type: "integer", minimum: 10, maximum: 300, description: "Maximum retained rows to show per diagnostics section." }
11989
+ }
11990
+ },
11991
+ actions: [
11992
+ {
11993
+ name: "refresh_snapshot",
11994
+ description: "Return a fresh summary of the tap diagnostics canvas data.",
11995
+ inputSchema: {
11996
+ type: "object",
11997
+ properties: {
11998
+ limit: { type: "integer", minimum: 10, maximum: 300 }
11999
+ }
12000
+ },
12001
+ handler: async ({ input }) => ({
12002
+ ok: true,
12003
+ summary: summarizeSnapshot(snapshot(input))
12004
+ })
12005
+ },
12006
+ {
12007
+ name: "export_snapshot",
12008
+ description: "Return the current tap diagnostics snapshot as structured JSON.",
12009
+ inputSchema: {
12010
+ type: "object",
12011
+ properties: {
12012
+ limit: { type: "integer", minimum: 10, maximum: 300 }
12013
+ }
12014
+ },
12015
+ handler: async ({ input }) => snapshot(input)
12016
+ }
12017
+ ],
12018
+ open: async (ctx) => {
12019
+ let entry = instances.get(ctx.instanceId);
12020
+ if (!entry) {
12021
+ entry = await startServer(ctx.instanceId);
12022
+ }
12023
+ return {
12024
+ title: "Tap diagnostics",
12025
+ status: "live flight recorder",
12026
+ url: entry.url
12027
+ };
12028
+ },
12029
+ onClose: async (ctx) => {
12030
+ await stopServer(ctx.instanceId);
12031
+ }
12032
+ });
12033
+ }
12034
+
10895
12035
  // src/tap-runtime/runtime-factory.mjs
10896
12036
  var PROVIDER_SHUTDOWN_DEADLINE_MS = 1e4;
10897
12037
  var EMITTER_SHUTDOWN_WAIT_TIMEOUT_MS = 1e4;
@@ -10917,10 +12057,15 @@ function createCopilotChannelsRuntime(options = {}) {
10917
12057
  ...options.runtimeServiceOptions ?? {},
10918
12058
  cwd: options.cwd,
10919
12059
  session: options.session,
12060
+ diagnostics: options.diagnostics,
10920
12061
  shutdownSession: () => handleSessionShutdown(activeSession)
10921
12062
  });
10922
- process.stderr.write(`[tap-runtime] init \u2014 cwd=${runtimeService.session.getBaseCwd()}
12063
+ function logRuntime(message, options2 = {}) {
12064
+ runtimeService.diagnostics?.log?.("runtime", message, options2);
12065
+ process.stderr.write(`[tap-runtime] ${message}
10923
12066
  `);
12067
+ }
12068
+ logRuntime(`init \u2014 cwd=${runtimeService.session.getBaseCwd()}`);
10924
12069
  const tools = createTools({ tools: runtimeService.tools });
10925
12070
  const hooks = createHooks({ runtime: runtimeService.hooks });
10926
12071
  const tapToolsFn = () => tools;
@@ -10930,8 +12075,19 @@ function createCopilotChannelsRuntime(options = {}) {
10930
12075
  deliverPush: runtimeService.provider.deliverPush,
10931
12076
  log: runtimeService.provider.log
10932
12077
  });
10933
- process.stderr.write(`[tap-runtime] gateway created
10934
- `);
12078
+ logRuntime("gateway created");
12079
+ function getDiagnosticSnapshot(options2 = {}) {
12080
+ return runtimeService.diagnostics.snapshot(options2, {
12081
+ gateway: typeof gateway.getDiagnosticState === "function" ? gateway.getDiagnosticState() : null,
12082
+ tools: gateway.isRunning() ? gateway.getAllTools(tools) : tools
12083
+ });
12084
+ }
12085
+ const canvases = [
12086
+ createTapDiagnosticsCanvas({
12087
+ getSnapshot: getDiagnosticSnapshot,
12088
+ diagnostics: runtimeService.diagnostics
12089
+ })
12090
+ ];
10935
12091
  gateway.onToolsChanged(runtimeService.provider.replaceSessionTools);
10936
12092
  let cleanupShutdownListener = () => {
10937
12093
  };
@@ -10958,7 +12114,7 @@ function createCopilotChannelsRuntime(options = {}) {
10958
12114
  return;
10959
12115
  }
10960
12116
  handled = true;
10961
- process.stderr.write("[tap-runtime] session.shutdown received \u2014 stopping runtime\n");
12117
+ logRuntime("session.shutdown received \u2014 stopping runtime");
10962
12118
  void handleSessionShutdown(session2).catch((error) => {
10963
12119
  logShutdownFailure(session2, error);
10964
12120
  });
@@ -11027,19 +12183,15 @@ function createCopilotChannelsRuntime(options = {}) {
11027
12183
  return;
11028
12184
  }
11029
12185
  try {
11030
- process.stderr.write(`[tap-runtime] starting gateway\u2026
11031
- `);
12186
+ logRuntime("starting gateway...");
11032
12187
  gateway.start();
11033
- process.stderr.write(`[tap-runtime] gateway started
11034
- `);
12188
+ logRuntime("gateway start requested");
11035
12189
  } catch (err) {
11036
- process.stderr.write(`[tap-runtime] gateway start failed: ${err?.message ?? err}
11037
- `);
12190
+ logRuntime(`gateway start request failed: ${err?.message ?? err}`, { level: "warning" });
11038
12191
  }
11039
12192
  }
11040
12193
  function attachSession(nextSession) {
11041
- process.stderr.write(`[tap-runtime] attachSession \u2014 id=${nextSession?.id ?? "(none)"}
11042
- `);
12194
+ logRuntime(`attachSession \u2014 id=${nextSession?.id ?? "(none)"}`);
11043
12195
  activeSession = nextSession ?? null;
11044
12196
  if (shutdownRecord?.settled) {
11045
12197
  shutdownRecord = null;
@@ -11052,18 +12204,22 @@ function createCopilotChannelsRuntime(options = {}) {
11052
12204
  attachSession,
11053
12205
  tools,
11054
12206
  hooks,
12207
+ canvases,
11055
12208
  stopAllEmitters,
11056
12209
  stopAllEmittersAndWait,
11057
12210
  handleSessionShutdown,
11058
12211
  appendStreamMessage: runtimeService.session.appendStreamMessage,
11059
12212
  getTools: () => gateway.isRunning() ? gateway.getAllTools(tools) : tools,
12213
+ getCanvases: () => canvases,
11060
12214
  DEFAULT_STREAM
11061
12215
  };
11062
12216
  }
11063
12217
 
11064
12218
  // src/extension.mjs
12219
+ var diagnostics = globalThis.__tapDiagnostics ??= createDiagnosticsStore();
11065
12220
  function tapLog(msg) {
11066
12221
  const ts = (/* @__PURE__ */ new Date()).toISOString();
12222
+ diagnostics.log("extension", msg);
11067
12223
  process.stderr.write(`[tap ${ts}] ${msg}
11068
12224
  `);
11069
12225
  }
@@ -11071,7 +12227,8 @@ tapLog(`extension.mjs loading \u2014 pid=${process.pid} cwd=${process.cwd()} SES
11071
12227
  var isResume = Boolean(globalThis.__tapRuntime);
11072
12228
  tapLog(`runtime ${isResume ? "resuming (cached)" : "creating (fresh)"}`);
11073
12229
  var runtime = globalThis.__tapRuntime ??= createCopilotChannelsRuntime({
11074
- cwd: process.cwd()
12230
+ cwd: process.cwd(),
12231
+ diagnostics
11075
12232
  });
11076
12233
  tapLog("runtime ready");
11077
12234
  tapLog("calling joinSession\u2026");
@@ -11079,6 +12236,7 @@ var session;
11079
12236
  try {
11080
12237
  session = await joinSession({
11081
12238
  tools: runtime.getTools(),
12239
+ canvases: runtime.getCanvases(),
11082
12240
  hooks: runtime.hooks
11083
12241
  });
11084
12242
  tapLog(`joinSession OK \u2014 session.id=${session.id ?? "(none)"}`);