pokertools-arena 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -114139,6 +114139,89 @@ var DECISION_SANITY_SCENARIOS = Object.freeze([
114139
114139
  }
114140
114140
  ]);
114141
114141
 
114142
+ // src/config/arena-config.js
114143
+ var BASE_CONFIG = {
114144
+ table: {
114145
+ minPlayers: 2,
114146
+ maxPlayers: 10,
114147
+ startingStack: 3e3,
114148
+ smallBlind: 25,
114149
+ bigBlind: 50,
114150
+ ante: 5,
114151
+ handsPerLevel: 8,
114152
+ blindMultiplier: 1.5
114153
+ },
114154
+ timing: {
114155
+ actionSeconds: 30,
114156
+ timeBankSeconds: 30,
114157
+ lowTimeSeconds: 5,
114158
+ lowTimeFraction: 0.25,
114159
+ betweenActionsMs: 250,
114160
+ betweenHandsMs: 700
114161
+ },
114162
+ limits: {
114163
+ startingStack: { min: 100 },
114164
+ smallBlind: { min: 1 },
114165
+ bigBlind: { min: 2 },
114166
+ ante: { min: 0 },
114167
+ handsPerLevel: { min: 1 },
114168
+ blindMultiplier: { min: 1.1, max: 3, step: 0.1 },
114169
+ actionSeconds: { min: 1, max: 120 },
114170
+ timeBankSeconds: { min: 0, max: 600 },
114171
+ lowTimeSeconds: { min: 0, max: 600 },
114172
+ lowTimeFraction: { min: 0, max: 1, step: 0.05 },
114173
+ betweenActionsMs: { min: 0, max: 5e3 },
114174
+ betweenHandsMs: { min: 0, max: 1e4 }
114175
+ },
114176
+ connections: {
114177
+ defaultKind: "openai",
114178
+ presets: {
114179
+ openai: "https://api.openai.com/v1",
114180
+ openrouter: "https://openrouter.ai/api/v1",
114181
+ typesafe: "https://api.typesafe.ai/v1/systemone"
114182
+ }
114183
+ },
114184
+ history: {
114185
+ recentDecisions: 12,
114186
+ snapshotEvents: 300,
114187
+ persistedEvents: 100
114188
+ }
114189
+ };
114190
+ function mergeConfig(base, override) {
114191
+ if (!override || typeof override !== "object" || Array.isArray(override)) return { ...base };
114192
+ const merged = { ...base };
114193
+ for (const [key, value] of Object.entries(override)) {
114194
+ if (value && typeof value === "object" && !Array.isArray(value) && base[key] && typeof base[key] === "object") {
114195
+ merged[key] = mergeConfig(base[key], value);
114196
+ } else if (value !== void 0) {
114197
+ merged[key] = value;
114198
+ }
114199
+ }
114200
+ return merged;
114201
+ }
114202
+ var ARENA_CONFIG = Object.freeze(mergeConfig(BASE_CONFIG, globalThis.__POKERTOOLS_ARENA_CONFIG__));
114203
+ var TABLE_DEFAULTS = Object.freeze(ARENA_CONFIG.table);
114204
+ var TIMING_DEFAULTS = Object.freeze(ARENA_CONFIG.timing);
114205
+ var CONFIG_LIMITS = Object.freeze(ARENA_CONFIG.limits);
114206
+ var CONNECTION_PRESETS = Object.freeze(ARENA_CONFIG.connections.presets);
114207
+ var HISTORY_CONFIG = Object.freeze(ARENA_CONFIG.history);
114208
+ var FORM_DEFAULTS = Object.freeze({ ...TABLE_DEFAULTS, ...TIMING_DEFAULTS });
114209
+ function applyConfiguredFormDefaults(form) {
114210
+ for (const [name, value] of Object.entries(FORM_DEFAULTS)) {
114211
+ const input = form?.elements?.namedItem(name);
114212
+ if (!input) continue;
114213
+ input.value = String(value);
114214
+ const limits = CONFIG_LIMITS[name] || {};
114215
+ for (const attribute of ["min", "max", "step"]) {
114216
+ if (limits[attribute] != null) input.setAttribute(attribute, String(limits[attribute]));
114217
+ }
114218
+ }
114219
+ }
114220
+ function defaultConnections() {
114221
+ const kind = ARENA_CONFIG.connections.defaultKind;
114222
+ return [{ name: "API", kind, baseUrl: CONNECTION_PRESETS[kind], apiKey: "", headers: "" }];
114223
+ }
114224
+
114142
114225
  // src/lib/decision-core.js
114143
114226
  var import_evaluator = __toESM(require_dist2(), 1);
114144
114227
  var ACTION = Object.freeze({
@@ -115561,7 +115644,6 @@ var els = {
115561
115644
  handValue: $("#handValue"),
115562
115645
  levelValue: $("#levelValue"),
115563
115646
  streetLabel: $("#streetLabel"),
115564
- winnerBanner: $("#winnerBanner"),
115565
115647
  decisionPanelTitle: $("#decisionPanelTitle"),
115566
115648
  decisionEmpty: $("#decisionEmpty"),
115567
115649
  decisionCard: $("#decisionCard"),
@@ -115582,8 +115664,10 @@ var els = {
115582
115664
  decisionLabelPosition: $("#decisionLabelPosition"),
115583
115665
  decisionLabelOptions: $("#decisionLabelOptions"),
115584
115666
  decisionFeed: $("#decisionFeed"),
115667
+ decisionFeedSummary: $("#decisionFeedSummary"),
115585
115668
  eventLog: $("#eventLog"),
115586
115669
  logSummary: $("#logSummary"),
115670
+ logTabCount: $("#logTabCount"),
115587
115671
  logSearch: $("#logSearch"),
115588
115672
  logFilter: $("#logFilter"),
115589
115673
  logClear: $("#logClear"),
@@ -115653,7 +115737,7 @@ var audioContext = null;
115653
115737
  var soundMasterGain = null;
115654
115738
  var lastVisualState = null;
115655
115739
  var lastProcessedEventId = null;
115656
- var MAX_LOBBY_SEATS = 10;
115740
+ var MAX_LOBBY_SEATS = TABLE_DEFAULTS.maxPlayers;
115657
115741
  var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
115658
115742
  var editingSeatIndex = null;
115659
115743
  var seatNameAuto = false;
@@ -115674,14 +115758,6 @@ var activeDecisionClockId = null;
115674
115758
  var tableRecording = null;
115675
115759
  var currentReplayEvent = null;
115676
115760
  var renderMemo = { status: "", table: "", decision: "", feed: "", events: "", stats: "" };
115677
- var TIMING_DEFAULTS = Object.freeze({
115678
- actionSeconds: 20,
115679
- timeBankSeconds: 30,
115680
- lowTimeSeconds: 5,
115681
- lowTimeFraction: 0.25,
115682
- betweenActionsMs: 250,
115683
- betweenHandsMs: 700
115684
- });
115685
115761
  function animationsAllowed() {
115686
115762
  return document.visibilityState === "visible" && !globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
115687
115763
  }
@@ -115702,13 +115778,6 @@ function soundMaster() {
115702
115778
  }
115703
115779
  return soundMasterGain;
115704
115780
  }
115705
- function recordingAudioTrack() {
115706
- const ctx = getAudioContext();
115707
- if (!ctx || typeof ctx.createMediaStreamDestination !== "function") return null;
115708
- const destination = ctx.createMediaStreamDestination();
115709
- soundMaster()?.connect(destination);
115710
- return { destination, track: destination.stream.getAudioTracks()[0] || null };
115711
- }
115712
115781
  function tone(freq, duration = 0.05, gain = 0.035, offset = 0, type = "sine") {
115713
115782
  if (!soundEnabled) return;
115714
115783
  const ctx = getAudioContext();
@@ -115939,10 +116008,6 @@ function processVisualEffects(s) {
115939
116008
  if (events.length) lastProcessedEventId = events.at(-1).id;
115940
116009
  lastVisualState = s;
115941
116010
  }
115942
- var defaultConnections = [
115943
- { name: "API", kind: "openai", baseUrl: "https://api.openai.com/v1", apiKey: "", headers: "" }
115944
- ];
115945
- var defaultPlayers = [];
115946
116011
  function injectedEnvironmentConfig() {
115947
116012
  const raw = globalThis.__POKERTOOLS_ENV__;
115948
116013
  if (!raw || typeof raw !== "object") return null;
@@ -115950,7 +116015,7 @@ function injectedEnvironmentConfig() {
115950
116015
  const players = Array.isArray(raw.players) ? raw.players.filter((p) => p && p.model) : [];
115951
116016
  if (!connections.length && !players.length) return null;
115952
116017
  const settings = raw.settings && typeof raw.settings === "object" ? {
115953
- startingStack: Math.max(100, Math.round(Number(raw.settings.startingStack) || 1e4)),
116018
+ startingStack: Math.max(CONFIG_LIMITS.startingStack.min, Math.round(Number(raw.settings.startingStack) || TABLE_DEFAULTS.startingStack)),
115954
116019
  autostart: Boolean(raw.settings.autostart),
115955
116020
  maxDecisions: Math.max(0, Math.round(Number(raw.settings.maxDecisions) || 0))
115956
116021
  } : null;
@@ -115960,7 +116025,7 @@ function injectedEnvironmentConfig() {
115960
116025
  id: String(c.id || `env-connection-${index + 1}`),
115961
116026
  name: String(c.name || "API"),
115962
116027
  kind: ["openai", "openrouter", "typesafe"].includes(c.kind) ? c.kind : "openai",
115963
- baseUrl: String(c.baseUrl || "https://api.openai.com/v1"),
116028
+ baseUrl: String(c.baseUrl || CONNECTION_PRESETS.openai),
115964
116029
  apiKey: String(c.apiKey || ""),
115965
116030
  headers: String(c.headers || "")
115966
116031
  })),
@@ -116205,7 +116270,7 @@ function buildBlindStructure(config) {
116205
116270
  let sb = Math.max(1, Math.round(config.smallBlind));
116206
116271
  let bb = Math.max(sb * 2, Math.round(config.bigBlind));
116207
116272
  let ante = Math.max(0, Math.round(config.ante || 0));
116208
- const multiplier = clamp(Number(config.blindMultiplier) || 1.5, 1.1, 3);
116273
+ const multiplier = clamp(Number(config.blindMultiplier) || TABLE_DEFAULTS.blindMultiplier, CONFIG_LIMITS.blindMultiplier.min, CONFIG_LIMITS.blindMultiplier.max);
116209
116274
  const safe = (value) => Number.isSafeInteger(value) && value >= 0;
116210
116275
  for (let i = 0; i < 60; i++) {
116211
116276
  if (!safe(sb) || !safe(bb) || !safe(ante)) break;
@@ -116222,7 +116287,7 @@ function buildBlindStructure(config) {
116222
116287
  return levels;
116223
116288
  }
116224
116289
  function normalizeConfig(input = {}) {
116225
- const players = Array.isArray(input.players) ? input.players.slice(0, 10) : [];
116290
+ const players = Array.isArray(input.players) ? input.players.slice(0, MAX_LOBBY_SEATS) : [];
116226
116291
  const connections = Array.isArray(input.connections) ? input.connections : [];
116227
116292
  if (players.length < 2) throw new Error("At least 2 players are required");
116228
116293
  if (!connections.length) throw new Error("Add at least one API connection");
@@ -116236,24 +116301,24 @@ function normalizeConfig(input = {}) {
116236
116301
  parseHeaders(c.headers);
116237
116302
  return { ...c, baseUrl: normalizeBaseUrl(c.baseUrl) };
116238
116303
  });
116239
- const smallBlind = Math.max(1, Math.round(Number(input.smallBlind) || 25));
116240
- const bigBlind = Math.max(2, Math.round(Number(input.bigBlind) || 50));
116304
+ const smallBlind = Math.max(CONFIG_LIMITS.smallBlind.min, Math.round(Number(input.smallBlind) || TABLE_DEFAULTS.smallBlind));
116305
+ const bigBlind = Math.max(CONFIG_LIMITS.bigBlind.min, Math.round(Number(input.bigBlind) || TABLE_DEFAULTS.bigBlind));
116241
116306
  if (bigBlind < smallBlind * 2) throw new Error("Big blind must be at least twice the small blind");
116242
116307
  return {
116243
116308
  id: id("tournament"),
116244
116309
  name: "pokertools-arena",
116245
- startingStack: Math.max(100, Math.round(Number(input.startingStack) || 1e4)),
116310
+ startingStack: Math.max(CONFIG_LIMITS.startingStack.min, Math.round(Number(input.startingStack) || TABLE_DEFAULTS.startingStack)),
116246
116311
  smallBlind,
116247
116312
  bigBlind,
116248
- ante: Math.max(0, Math.round(Number(input.ante) || 0)),
116249
- handsPerLevel: Math.max(1, Math.round(Number(input.handsPerLevel) || 8)),
116250
- blindMultiplier: clamp(Number(input.blindMultiplier) || 1.5, 1.1, 3),
116251
- actionSeconds: clamp(Number(input.actionSeconds) || TIMING_DEFAULTS.actionSeconds, 1, 120),
116252
- timeBankSeconds: clamp(Number(input.timeBankSeconds) || TIMING_DEFAULTS.timeBankSeconds, 0, 600),
116253
- lowTimeSeconds: clamp(Number.isFinite(Number(input.lowTimeSeconds)) ? Number(input.lowTimeSeconds) : TIMING_DEFAULTS.lowTimeSeconds, 0, 600),
116254
- lowTimeFraction: clamp(Number.isFinite(Number(input.lowTimeFraction)) ? Number(input.lowTimeFraction) : TIMING_DEFAULTS.lowTimeFraction, 0, 1),
116255
- betweenActionsMs: clamp(Number(input.betweenActionsMs) || TIMING_DEFAULTS.betweenActionsMs, 0, 5e3),
116256
- betweenHandsMs: clamp(Number(input.betweenHandsMs) || TIMING_DEFAULTS.betweenHandsMs, 0, 1e4),
116313
+ ante: Math.max(CONFIG_LIMITS.ante.min, Math.round(Number(input.ante) || TABLE_DEFAULTS.ante)),
116314
+ handsPerLevel: Math.max(CONFIG_LIMITS.handsPerLevel.min, Math.round(Number(input.handsPerLevel) || TABLE_DEFAULTS.handsPerLevel)),
116315
+ blindMultiplier: clamp(Number(input.blindMultiplier) || TABLE_DEFAULTS.blindMultiplier, CONFIG_LIMITS.blindMultiplier.min, CONFIG_LIMITS.blindMultiplier.max),
116316
+ actionSeconds: clamp(Number(input.actionSeconds) || TIMING_DEFAULTS.actionSeconds, CONFIG_LIMITS.actionSeconds.min, CONFIG_LIMITS.actionSeconds.max),
116317
+ timeBankSeconds: clamp(Number(input.timeBankSeconds) || TIMING_DEFAULTS.timeBankSeconds, CONFIG_LIMITS.timeBankSeconds.min, CONFIG_LIMITS.timeBankSeconds.max),
116318
+ lowTimeSeconds: clamp(Number.isFinite(Number(input.lowTimeSeconds)) ? Number(input.lowTimeSeconds) : TIMING_DEFAULTS.lowTimeSeconds, CONFIG_LIMITS.lowTimeSeconds.min, CONFIG_LIMITS.lowTimeSeconds.max),
116319
+ lowTimeFraction: clamp(Number.isFinite(Number(input.lowTimeFraction)) ? Number(input.lowTimeFraction) : TIMING_DEFAULTS.lowTimeFraction, CONFIG_LIMITS.lowTimeFraction.min, CONFIG_LIMITS.lowTimeFraction.max),
116320
+ betweenActionsMs: clamp(Number(input.betweenActionsMs) || TIMING_DEFAULTS.betweenActionsMs, CONFIG_LIMITS.betweenActionsMs.min, CONFIG_LIMITS.betweenActionsMs.max),
116321
+ betweenHandsMs: clamp(Number(input.betweenHandsMs) || TIMING_DEFAULTS.betweenHandsMs, CONFIG_LIMITS.betweenHandsMs.min, CONFIG_LIMITS.betweenHandsMs.max),
116257
116322
  connections: cleanConnections,
116258
116323
  benchmarkMode: input.benchmarkMode === BENCHMARK_MODES.RAW ? BENCHMARK_MODES.RAW : DEFAULT_BENCHMARK_MODE,
116259
116324
  decisionArchitecture: input.decisionArchitecture === DECISION_ARCHITECTURES.FLAT ? DECISION_ARCHITECTURES.FLAT : DEFAULT_DECISION_ARCHITECTURE,
@@ -116427,7 +116492,7 @@ var TournamentDirector = class {
116427
116492
  stats: this.stats,
116428
116493
  publicPlayerStats,
116429
116494
  timeBanks: this.timeBanks,
116430
- events: this.events.slice(-300)
116495
+ events: this.events.slice(-HISTORY_CONFIG.snapshotEvents)
116431
116496
  };
116432
116497
  }
116433
116498
  broadcast() {
@@ -116449,7 +116514,7 @@ var TournamentDirector = class {
116449
116514
  persistLightweight() {
116450
116515
  try {
116451
116516
  const state = this.snapshot();
116452
- const lightweight = { ...state, events: state.events.slice(-100), currentDecision: null };
116517
+ const lightweight = { ...state, events: state.events.slice(-HISTORY_CONFIG.persistedEvents), currentDecision: null };
116453
116518
  storageSet("pokertoolsArenaLastState", JSON.stringify(lightweight));
116454
116519
  } catch {
116455
116520
  }
@@ -116838,16 +116903,12 @@ function refreshSeatConnectionSelect(selected = null) {
116838
116903
  applySeatProtocolRules();
116839
116904
  }
116840
116905
  function connectionPresetUrl(kind) {
116841
- if (kind === "openrouter") return "https://openrouter.ai/api/v1";
116842
- if (kind === "typesafe") return "https://api.typesafe.ai/v1/systemone";
116843
- return "https://api.openai.com/v1";
116906
+ return CONNECTION_PRESETS[kind] || CONNECTION_PRESETS.openai;
116844
116907
  }
116845
116908
  function shouldReplacePresetUrl(value) {
116846
116909
  const normalized = normalizeBaseUrl(value);
116847
116910
  return !normalized || [
116848
- "https://api.openai.com/v1",
116849
- "https://openrouter.ai/api/v1",
116850
- "https://api.typesafe.ai/v1/systemone"
116911
+ ...Object.values(CONNECTION_PRESETS)
116851
116912
  ].includes(normalized);
116852
116913
  }
116853
116914
  function addConnectionRow(connection = {}) {
@@ -116871,9 +116932,9 @@ function addConnectionRow(connection = {}) {
116871
116932
  $("[data-field=kind]", row).addEventListener("change", () => {
116872
116933
  const kind = $("[data-field=kind]", row).value;
116873
116934
  const url = $("[data-field=baseUrl]", row);
116874
- if (kind === "typesafe" && (!url.value || url.value.includes("openrouter.ai"))) url.value = "https://api.typesafe.ai/v1/systemone";
116875
- if (kind === "openrouter" && (!url.value || url.value.includes("typesafe.ai"))) url.value = "https://openrouter.ai/api/v1";
116876
- if (kind === "openai" && (!url.value || url.value.includes("typesafe.ai"))) url.value = "https://api.openai.com/v1";
116935
+ if (kind === "typesafe" && (!url.value || url.value.includes("openrouter.ai"))) url.value = CONNECTION_PRESETS.typesafe;
116936
+ if (kind === "openrouter" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openrouter;
116937
+ if (kind === "openai" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openai;
116877
116938
  refreshSeatConnectionSelect();
116878
116939
  });
116879
116940
  $(".remove-connection", row).addEventListener("click", () => {
@@ -117096,7 +117157,7 @@ function saveSeatAssignments() {
117096
117157
  }
117097
117158
  function restoreSeatAssignments(rows = []) {
117098
117159
  seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
117099
- const sources = rows.length ? rows : defaultPlayers;
117160
+ const sources = rows;
117100
117161
  sources.slice(0, MAX_LOBBY_SEATS).forEach((player, index) => {
117101
117162
  const lobbySeat = clamp(Math.round(Number(player.lobbySeat ?? index)), 0, MAX_LOBBY_SEATS - 1);
117102
117163
  let connectionId = player.connectionId || "";
@@ -117166,13 +117227,14 @@ async function primeModelCapabilities() {
117166
117227
  }));
117167
117228
  }
117168
117229
  function restoreSetup() {
117230
+ applyConfiguredFormDefaults(els.setupForm);
117169
117231
  let saved = null;
117170
117232
  try {
117171
117233
  saved = JSON.parse(storageGet("pokertoolsArenaBrowserConfig"));
117172
117234
  } catch {
117173
117235
  }
117174
117236
  const injected = injectedEnvironmentConfig();
117175
- const conns = injected?.connections?.length ? injected.connections : saved?.connections?.length ? saved.connections : defaultConnections;
117237
+ const conns = injected?.connections?.length ? injected.connections : saved?.connections?.length ? saved.connections : defaultConnections();
117176
117238
  conns.forEach(addConnectionRow);
117177
117239
  restoreSeatAssignments(injected?.players?.length ? injected.players : saved?.players?.length ? saved.players : []);
117178
117240
  capabilityPrime = primeModelCapabilities();
@@ -117198,7 +117260,7 @@ function restoreSetup() {
117198
117260
  function renderLobbyIfVisible() {
117199
117261
  if (lobbyVisible && (!director || !["RUNNING", "PAUSED"].includes(director.status))) renderTable(currentState || { status: "IDLE", events: [] });
117200
117262
  }
117201
- var TABLE_MIN_PLAYERS = 2;
117263
+ var TABLE_MIN_PLAYERS = TABLE_DEFAULTS.minPlayers;
117202
117264
  function visualSeatAngle(index, count) {
117203
117265
  const safeCount = Math.max(TABLE_MIN_PLAYERS, Math.min(MAX_LOBBY_SEATS, Math.round(Number(count) || TABLE_MIN_PLAYERS)));
117204
117266
  const safeIndex = (Math.round(Number(index) || 0) % safeCount + safeCount) % safeCount;
@@ -117341,7 +117403,6 @@ function renderLobbyTable() {
117341
117403
  els.handValue.textContent = "\u2014";
117342
117404
  els.levelValue.textContent = "\u2014";
117343
117405
  els.streetLabel.textContent = "LOBBY";
117344
- els.winnerBanner.classList.add("hidden");
117345
117406
  }
117346
117407
  function renderStatus(s) {
117347
117408
  const status = s?.status || "IDLE";
@@ -117412,7 +117473,6 @@ function renderTable(s) {
117412
117473
  els.levelValue.title = `Level ${Number(table.blindLevel ?? 0) + 1}`;
117413
117474
  els.streetLabel.textContent = s.status === "FINISHED" && s.winner ? `WINNER \xB7 ${displayModelName(s.winner.model || "")}` : table.street || "\u2014";
117414
117475
  els.streetLabel.classList.toggle("winner-street", s.status === "FINISHED" && Boolean(s.winner));
117415
- els.winnerBanner.classList.add("hidden");
117416
117476
  }
117417
117477
  function clearTurnRings(keep = null) {
117418
117478
  for (const el of $$(".seat.turn-active")) if (el !== keep) el.classList.remove("turn-active");
@@ -117651,7 +117711,7 @@ function renderDecision(s) {
117651
117711
  }
117652
117712
  function renderFeed(s) {
117653
117713
  const feedArchive = eventArchive(s);
117654
- const events = feedArchive.filter((e) => e.type === "DECISION").slice(-12).reverse();
117714
+ const events = feedArchive.filter((e) => e.type === "DECISION").slice(-HISTORY_CONFIG.recentDecisions).reverse();
117655
117715
  const explanations = new Map(feedArchive.filter((e) => e.type === "SPECTATOR_EXPLANATION").map((e) => [e.decisionId, e.text]));
117656
117716
  els.decisionFeed.innerHTML = events.length ? events.map((e) => {
117657
117717
  const infra = [];
@@ -117663,6 +117723,7 @@ function renderFeed(s) {
117663
117723
  const reason = explanations.get(e.decisionId) || e.publicReason || (e.decisionMeta?.family ? "Typed decision (no text rationale)" : "No public rationale");
117664
117724
  return `<button type="button" class="decision-item decision-history-item ${e.error ? "error" : ""}" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))} decision"><div class="decision-item-head"><strong title="${escapeHtml(e.configuredModel || e.resolvedModel || "")}">${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))}</strong><span class="decision-item-action">${escapeHtml(e.action?.description || "\u2014")}</span></div><div class="decision-reason">${escapeHtml(reason)}</div>${telemetry ? `<div class="decision-item-telemetry">${telemetry}</div>` : ""}<div class="decision-meta mono">${escapeHtml(e.connection || "")} \xB7 ${escapeHtml(shortModel(e.resolvedModel || e.configuredModel || ""))} \xB7 ${e.primaryDecisionLatencyMs || e.latencyMs || 0}ms${infra.length ? ` \xB7 ${escapeHtml(infra.join(" \xB7 "))}` : ""}${e.error ? ` \xB7 ${escapeHtml(e.error)}` : ""}<span class="decision-replay-hint">View hand \u2197</span></div></button>`;
117665
117725
  }).join("") : '<div class="empty-state">No decisions yet.</div>';
117726
+ if (els.decisionFeedSummary) els.decisionFeedSummary.textContent = events.length ? `${events.length} most recent` : "Newest first";
117666
117727
  }
117667
117728
  function eventArchive(s = currentState) {
117668
117729
  if (director?.events?.length) return director.events;
@@ -117714,6 +117775,7 @@ function eventMatchesView(event) {
117714
117775
  function renderEvents(s) {
117715
117776
  const archive = eventArchive(s);
117716
117777
  const events = archive.filter(eventMatchesView).reverse();
117778
+ if (els.logTabCount) els.logTabCount.textContent = archive.length > 999 ? "999+" : String(archive.length);
117717
117779
  if (els.logSummary) {
117718
117780
  const suffix = events.length === archive.length ? "full run" : `${events.length} shown`;
117719
117781
  els.logSummary.textContent = `${archive.length.toLocaleString()} event${archive.length === 1 ? "" : "s"} \xB7 ${suffix}`;
@@ -117728,7 +117790,7 @@ function renderEvents(s) {
117728
117790
  const attrs = replayable ? ` type="button" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(detail || "decision")}"` : "";
117729
117791
  return `<${tag}${attrs} class="event-row event-${category}${e.error || e.type?.includes?.("ERROR") ? " has-error" : ""}">
117730
117792
  <span class="event-rail" aria-hidden="true"></span>
117731
- <span class="event-main"><span class="event-row-head"><span class="event-type">${escapeHtml(e.type)}</span><time class="event-time mono" datetime="${new Date(e.at).toISOString()}">${escapeHtml(time)}</time></span>${detail ? `<span class="event-detail">${escapeHtml(detail)}</span>` : ""}${meta ? `<span class="event-meta mono">${escapeHtml(meta)}</span>` : ""}</span>
117793
+ <span class="event-main"><span class="event-row-head"><span class="event-type">${escapeHtml(e.type.replaceAll("_", " "))}</span><time class="event-time mono" datetime="${new Date(e.at).toISOString()}">${escapeHtml(time)}</time></span>${detail ? `<span class="event-detail">${escapeHtml(detail)}</span>` : ""}${meta ? `<span class="event-meta mono">${escapeHtml(meta)}</span>` : ""}</span>
117732
117794
  ${replayable ? '<span class="event-open" aria-hidden="true">\u2197</span>' : ""}
117733
117795
  </${tag}>`;
117734
117796
  }).join("") || '<div class="empty-state log-empty">No events match these filters.</div>';
@@ -117791,6 +117853,10 @@ function statsRenderSignature(s) {
117791
117853
  }
117792
117854
  function render(s, { force = false } = {}) {
117793
117855
  currentState = s;
117856
+ if (els.logTabCount) {
117857
+ const eventCount = eventArchive(s).length;
117858
+ els.logTabCount.textContent = eventCount > 999 ? "999+" : String(eventCount);
117859
+ }
117794
117860
  const statusSig = statusRenderSignature(s);
117795
117861
  if (force || renderMemo.status !== statusSig) {
117796
117862
  renderMemo.status = statusSig;
@@ -118000,26 +118066,26 @@ function openTests() {
118000
118066
  }
118001
118067
  var TABLE_RECORDING_CONFIG = Object.freeze({
118002
118068
  frameRate: 30,
118003
- maxWidth: 1920,
118004
- maxHeight: 1080,
118005
- minBitrate: 6e6,
118006
- maxBitrate: 16e6,
118007
- bitsPerPixelPerFrame: 0.14,
118008
- audioBitsPerSecond: 128e3,
118069
+ maxWidth: 2560,
118070
+ maxHeight: 1440,
118071
+ minBitrate: 1e7,
118072
+ maxBitrate: 28e6,
118073
+ bitsPerPixelPerFrame: 0.2,
118074
+ audioBitsPerSecond: 192e3,
118009
118075
  background: "#07090b"
118010
118076
  });
118011
- function recorderMimeType() {
118077
+ function recorderMimeType(hasAudio = true) {
118012
118078
  if (!globalThis.MediaRecorder) return "";
118013
- for (const type of [
118079
+ const candidates = hasAudio ? [
118014
118080
  "video/webm;codecs=vp8,opus",
118015
118081
  "video/webm;codecs=vp9,opus",
118082
+ "video/webm"
118083
+ ] : [
118016
118084
  "video/webm;codecs=vp8",
118017
118085
  "video/webm;codecs=vp9",
118018
118086
  "video/webm"
118019
- ]) {
118020
- if (MediaRecorder.isTypeSupported?.(type)) return type;
118021
- }
118022
- return "";
118087
+ ];
118088
+ return candidates.find((type) => MediaRecorder.isTypeSupported?.(type)) || "";
118023
118089
  }
118024
118090
  function recordingVideoBitrate({ width, height, frameRate = TABLE_RECORDING_CONFIG.frameRate }) {
118025
118091
  const pixelsPerSecond = Math.max(1, width) * Math.max(1, height) * Math.max(1, frameRate);
@@ -118037,7 +118103,7 @@ function setRecordButton(active, label = null) {
118037
118103
  const text = $(".action-label", els.recordBtn);
118038
118104
  if (icon) icon.textContent = active ? "\u25A0" : "\u25CF";
118039
118105
  if (text) text.textContent = label || (active ? "Stop rec" : "Record");
118040
- els.recordBtn.title = active ? "Stop table recording and save video" : "Record only the poker table";
118106
+ els.recordBtn.title = active ? "Stop table recording and save video" : "Record poker table with current-tab audio";
118041
118107
  els.recordBtn.setAttribute("aria-label", els.recordBtn.title);
118042
118108
  }
118043
118109
  function saveRecordingBlob(blob) {
@@ -118068,72 +118134,26 @@ function fitRecordingSize(sourceWidth, sourceHeight) {
118068
118134
  height: evenRecordingDimension(height * scale)
118069
118135
  };
118070
118136
  }
118071
- function waitForCaptureVideo(video, track) {
118072
- return new Promise((resolve, reject) => {
118073
- let settled = false;
118074
- const finish = (callback, value) => {
118075
- if (settled) return;
118076
- settled = true;
118077
- clearTimeout(timeoutId);
118078
- track?.removeEventListener?.("ended", onEnded);
118079
- video.removeEventListener("loadedmetadata", onReady);
118080
- video.removeEventListener("resize", onReady);
118081
- callback(value);
118082
- };
118083
- const onReady = () => {
118084
- if (video.videoWidth > 0 && video.videoHeight > 0) finish(resolve);
118085
- };
118086
- const onEnded = () => finish(reject, new Error("Screen sharing ended before recording started"));
118087
- const timeoutId = setTimeout(
118088
- () => finish(reject, new Error("Timed out waiting for the shared tab video")),
118089
- 8e3
118090
- );
118091
- track?.addEventListener?.("ended", onEnded, { once: true });
118092
- video.addEventListener("loadedmetadata", onReady);
118093
- video.addEventListener("resize", onReady);
118094
- onReady();
118095
- });
118096
- }
118097
- function createCaptureVideo(stream) {
118098
- const video = document.createElement("video");
118099
- video.muted = true;
118100
- video.playsInline = true;
118101
- video.autoplay = true;
118102
- video.setAttribute("aria-hidden", "true");
118103
- Object.assign(video.style, {
118104
- position: "fixed",
118105
- left: "0",
118106
- top: "0",
118107
- width: "2px",
118108
- height: "2px",
118109
- opacity: "0.001",
118110
- pointerEvents: "none",
118111
- zIndex: "2147483647"
118112
- });
118113
- video.srcObject = stream;
118114
- document.body.append(video);
118115
- return video;
118116
- }
118117
- function captureViewportMetrics(video) {
118137
+ function captureViewportMetrics(sourceWidth, sourceHeight) {
118118
118138
  const viewport = globalThis.visualViewport;
118119
118139
  const cssWidth = Math.max(1, viewport?.width || document.documentElement.clientWidth || innerWidth || 1);
118120
118140
  const cssHeight = Math.max(1, viewport?.height || document.documentElement.clientHeight || innerHeight || 1);
118121
- const sourceWidth = Math.max(2, video.videoWidth || cssWidth);
118122
- const sourceHeight = Math.max(2, video.videoHeight || cssHeight);
118141
+ const width = Math.max(2, Number(sourceWidth) || cssWidth);
118142
+ const height = Math.max(2, Number(sourceHeight) || cssHeight);
118123
118143
  return {
118124
118144
  cssWidth,
118125
118145
  cssHeight,
118126
- sourceWidth,
118127
- sourceHeight,
118128
- scaleX: sourceWidth / cssWidth,
118129
- scaleY: sourceHeight / cssHeight,
118146
+ sourceWidth: width,
118147
+ sourceHeight: height,
118148
+ scaleX: width / cssWidth,
118149
+ scaleY: height / cssHeight,
118130
118150
  offsetX: viewport?.offsetLeft || 0,
118131
118151
  offsetY: viewport?.offsetTop || 0
118132
118152
  };
118133
118153
  }
118134
- function tableCropSourceRect(video) {
118154
+ function tableCropSourceRect(sourceWidth, sourceHeight) {
118135
118155
  const rect = els.pokerTable.getBoundingClientRect();
118136
- const metrics = captureViewportMetrics(video);
118156
+ const metrics = captureViewportMetrics(sourceWidth, sourceHeight);
118137
118157
  const leftCss = rect.left - metrics.offsetX;
118138
118158
  const topCss = rect.top - metrics.offsetY;
118139
118159
  const sx = clamp(Math.round(leftCss * metrics.scaleX), 0, Math.max(0, metrics.sourceWidth - 2));
@@ -118142,73 +118162,197 @@ function tableCropSourceRect(video) {
118142
118162
  const sh = clamp(Math.round(rect.height * metrics.scaleY), 2, metrics.sourceHeight - sy);
118143
118163
  return { sx, sy, sw, sh };
118144
118164
  }
118145
- function createTableRecordingCanvas(video) {
118146
- const source = tableCropSourceRect(video);
118165
+ function createTableRecordingCanvas(sourceWidth, sourceHeight) {
118166
+ const source = tableCropSourceRect(sourceWidth, sourceHeight);
118147
118167
  const output = fitRecordingSize(source.sw, source.sh);
118148
118168
  const canvas = document.createElement("canvas");
118149
118169
  canvas.width = output.width;
118150
118170
  canvas.height = output.height;
118151
- const ctx = canvas.getContext("2d", { alpha: false, desynchronized: true });
118171
+ const ctx = canvas.getContext("2d", { alpha: false });
118152
118172
  if (!ctx) throw new Error("Canvas video recording is not available in this browser");
118153
118173
  ctx.imageSmoothingEnabled = true;
118154
118174
  ctx.imageSmoothingQuality = "high";
118155
118175
  return { canvas, ctx };
118156
118176
  }
118157
- function createCanvasRecordingStream(canvas) {
118158
- const stream = canvas.captureStream(TABLE_RECORDING_CONFIG.frameRate);
118159
- const track = stream.getVideoTracks()[0];
118160
- if (!track) throw new Error("Could not create the table recording video track");
118161
- return { stream, track };
118162
- }
118163
- function paintTableRecordingFrame(recording) {
118164
- const { ctx, canvas, captureVideo } = recording;
118165
- if (!ctx || !canvas || !captureVideo || captureVideo.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return false;
118166
- const source = tableCropSourceRect(captureVideo);
118167
- ctx.save();
118177
+ function paintTableRecordingFrame(recording, source, sourceWidth, sourceHeight) {
118178
+ const { ctx, canvas } = recording;
118179
+ if (!ctx || !canvas || !source) return false;
118180
+ const crop = tableCropSourceRect(sourceWidth, sourceHeight);
118168
118181
  ctx.globalCompositeOperation = "copy";
118169
118182
  ctx.fillStyle = TABLE_RECORDING_CONFIG.background;
118170
118183
  ctx.fillRect(0, 0, canvas.width, canvas.height);
118171
118184
  try {
118172
118185
  ctx.drawImage(
118173
- captureVideo,
118174
- source.sx,
118175
- source.sy,
118176
- source.sw,
118177
- source.sh,
118186
+ source,
118187
+ crop.sx,
118188
+ crop.sy,
118189
+ crop.sw,
118190
+ crop.sh,
118178
118191
  0,
118179
118192
  0,
118180
118193
  canvas.width,
118181
118194
  canvas.height
118182
118195
  );
118196
+ return true;
118183
118197
  } catch {
118184
- ctx.restore();
118185
118198
  return false;
118199
+ } finally {
118200
+ ctx.globalCompositeOperation = "source-over";
118186
118201
  }
118187
- ctx.restore();
118188
- return true;
118189
118202
  }
118190
- function startTableRecordingPainter(recording) {
118203
+ function createCaptureVideo(stream) {
118204
+ const video = document.createElement("video");
118205
+ video.muted = true;
118206
+ video.playsInline = true;
118207
+ video.autoplay = true;
118208
+ video.setAttribute("aria-hidden", "true");
118209
+ Object.assign(video.style, {
118210
+ position: "fixed",
118211
+ inset: "0 auto auto 0",
118212
+ width: "2px",
118213
+ height: "2px",
118214
+ opacity: "0.001",
118215
+ pointerEvents: "none",
118216
+ zIndex: "-1"
118217
+ });
118218
+ video.srcObject = stream;
118219
+ document.body.append(video);
118220
+ return video;
118221
+ }
118222
+ function waitForCaptureVideo(video, track) {
118223
+ return new Promise((resolve, reject) => {
118224
+ let settled = false;
118225
+ const finish = (callback, value) => {
118226
+ if (settled) return;
118227
+ settled = true;
118228
+ clearTimeout(timeoutId);
118229
+ track?.removeEventListener?.("ended", onEnded);
118230
+ video.removeEventListener("loadedmetadata", onReady);
118231
+ video.removeEventListener("resize", onReady);
118232
+ callback(value);
118233
+ };
118234
+ const onReady = () => {
118235
+ if (video.videoWidth > 0 && video.videoHeight > 0) finish(resolve);
118236
+ };
118237
+ const onEnded = () => finish(reject, new Error("Screen sharing ended before recording started"));
118238
+ const timeoutId = setTimeout(
118239
+ () => finish(reject, new Error("Timed out waiting for the shared tab video")),
118240
+ 8e3
118241
+ );
118242
+ track?.addEventListener?.("ended", onEnded, { once: true });
118243
+ video.addEventListener("loadedmetadata", onReady);
118244
+ video.addEventListener("resize", onReady);
118245
+ onReady();
118246
+ });
118247
+ }
118248
+ function supportsTimestampedVideoPipeline() {
118249
+ return typeof globalThis.MediaStreamTrackProcessor === "function" && typeof globalThis.MediaStreamTrackGenerator === "function" && typeof globalThis.VideoFrame === "function";
118250
+ }
118251
+ function createTimestampedVideoPipeline(recording) {
118252
+ if (!supportsTimestampedVideoPipeline()) return null;
118253
+ try {
118254
+ const processor = new MediaStreamTrackProcessor({ track: recording.captureTrack });
118255
+ const generator = new MediaStreamTrackGenerator({ kind: "video" });
118256
+ const reader = processor.readable.getReader();
118257
+ const writer = generator.writable.getWriter();
118258
+ generator.contentHint = "detail";
118259
+ recording.processorReader = reader;
118260
+ recording.generatorWriter = writer;
118261
+ recording.outputVideoTrack = generator;
118262
+ return generator;
118263
+ } catch {
118264
+ recording.processorReader = null;
118265
+ recording.generatorWriter = null;
118266
+ recording.outputVideoTrack = null;
118267
+ return null;
118268
+ }
118269
+ }
118270
+ function startTimestampedVideoPump(recording) {
118271
+ const reader = recording.processorReader;
118272
+ const writer = recording.generatorWriter;
118273
+ if (!reader || !writer) return;
118274
+ const fallbackDuration = Math.round(1e6 / TABLE_RECORDING_CONFIG.frameRate);
118275
+ recording.videoPumpPromise = (async () => {
118276
+ try {
118277
+ while (!recording.stopping) {
118278
+ const { value: frame, done } = await reader.read();
118279
+ if (done || !frame) break;
118280
+ try {
118281
+ const width = frame.displayWidth || frame.codedWidth || recording.sourceWidth;
118282
+ const height = frame.displayHeight || frame.codedHeight || recording.sourceHeight;
118283
+ if (!paintTableRecordingFrame(recording, frame, width, height)) continue;
118284
+ if (writer.desiredSize != null && writer.desiredSize <= 0) continue;
118285
+ const timestamp = Number.isFinite(frame.timestamp) ? frame.timestamp : Math.round(performance.now() * 1e3);
118286
+ const duration = Number.isFinite(frame.duration) && frame.duration > 0 ? frame.duration : fallbackDuration;
118287
+ const outputFrame = new VideoFrame(recording.canvas, { timestamp, duration });
118288
+ try {
118289
+ await writer.write(outputFrame);
118290
+ } finally {
118291
+ outputFrame.close();
118292
+ }
118293
+ } finally {
118294
+ frame.close();
118295
+ }
118296
+ }
118297
+ } catch (error) {
118298
+ if (!recording.stopping && tableRecording === recording) {
118299
+ recording.pipelineError = error;
118300
+ if (recording.recorder?.state !== "inactive") recording.recorder.stop();
118301
+ }
118302
+ }
118303
+ })();
118304
+ }
118305
+ function createFallbackCanvasPipeline(recording) {
118306
+ const stream = recording.canvas.captureStream(TABLE_RECORDING_CONFIG.frameRate);
118307
+ const track = stream.getVideoTracks()[0];
118308
+ if (!track) throw new Error("Could not create the table recording video track");
118309
+ track.contentHint = "detail";
118310
+ recording.canvasStream = stream;
118311
+ recording.outputVideoTrack = track;
118312
+ return track;
118313
+ }
118314
+ function startFallbackVideoPump(recording) {
118191
118315
  const frameInterval = 1e3 / TABLE_RECORDING_CONFIG.frameRate;
118192
118316
  let previousPaint = -frameInterval;
118193
118317
  const loop = (timestamp) => {
118194
118318
  if (recording.stopping || tableRecording !== recording) return;
118195
118319
  if (timestamp - previousPaint >= frameInterval - 1) {
118196
- paintTableRecordingFrame(recording);
118320
+ const video2 = recording.captureVideo;
118321
+ if (video2?.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
118322
+ paintTableRecordingFrame(recording, video2, video2.videoWidth, video2.videoHeight);
118323
+ }
118197
118324
  previousPaint = timestamp;
118198
118325
  }
118199
118326
  recording.animationFrameId = requestAnimationFrame(loop);
118200
118327
  };
118201
- paintTableRecordingFrame(recording);
118328
+ const video = recording.captureVideo;
118329
+ if (video?.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
118330
+ paintTableRecordingFrame(recording, video, video.videoWidth, video.videoHeight);
118331
+ }
118202
118332
  recording.animationFrameId = requestAnimationFrame(loop);
118203
118333
  }
118204
- function stopTableRecordingPainter(recording) {
118334
+ function startTableRecordingVideo(recording) {
118335
+ if (recording.processorReader) startTimestampedVideoPump(recording);
118336
+ else startFallbackVideoPump(recording);
118337
+ }
118338
+ function stopTableRecordingVideo(recording) {
118205
118339
  if (!recording) return;
118206
118340
  if (recording.animationFrameId != null) cancelAnimationFrame(recording.animationFrameId);
118207
118341
  recording.animationFrameId = null;
118342
+ try {
118343
+ recording.processorReader?.cancel?.();
118344
+ } catch {
118345
+ }
118346
+ try {
118347
+ recording.generatorWriter?.close?.();
118348
+ } catch {
118349
+ }
118350
+ recording.processorReader = null;
118351
+ recording.generatorWriter = null;
118208
118352
  }
118209
- function disposeTableRecording(recording, { stopCapture = true, stopCanvas = true } = {}) {
118353
+ function disposeTableRecording(recording) {
118210
118354
  if (!recording) return;
118211
- stopTableRecordingPainter(recording);
118355
+ stopTableRecordingVideo(recording);
118212
118356
  try {
118213
118357
  recording.captureVideo?.pause?.();
118214
118358
  } catch {
@@ -118217,28 +118361,28 @@ function disposeTableRecording(recording, { stopCapture = true, stopCanvas = tru
118217
118361
  recording.captureVideo.srcObject = null;
118218
118362
  recording.captureVideo.remove();
118219
118363
  }
118220
- if (stopCanvas) recording.canvasStream?.getTracks?.().forEach((track) => track.stop());
118221
- if (stopCapture) recording.captureStream?.getTracks?.().forEach((track) => track.stop());
118222
- try {
118223
- if (soundMasterGain && recording.audioDestination) soundMasterGain.disconnect(recording.audioDestination);
118224
- } catch {
118225
- }
118364
+ recording.canvasStream?.getTracks?.().forEach((track) => track.stop());
118226
118365
  try {
118227
- recording.audioTrack?.stop?.();
118366
+ recording.outputVideoTrack?.stop?.();
118228
118367
  } catch {
118229
118368
  }
118369
+ recording.captureStream?.getTracks?.().forEach((track) => track.stop());
118230
118370
  recording.canvas?.remove?.();
118231
118371
  }
118232
118372
  function finalizeTableRecording(recording, mimeType) {
118233
118373
  const blob = new Blob(recording.chunks, { type: recording.recorder.mimeType || mimeType || "video/webm" });
118374
+ const error = recording.pipelineError;
118234
118375
  disposeTableRecording(recording);
118235
118376
  if (tableRecording === recording) tableRecording = null;
118236
118377
  setRecordButton(false);
118237
118378
  if (blob.size) saveRecordingBlob(blob);
118379
+ if (error) showActionToast(`Recording stopped: ${summarizeError(error)}`, "fold");
118238
118380
  }
118239
118381
  async function startTableRecording() {
118240
118382
  if (tableRecording || !els.pokerTable) return;
118241
- if (!navigator.mediaDevices?.getDisplayMedia || !globalThis.MediaRecorder || !HTMLCanvasElement.prototype.captureStream) {
118383
+ const canTimestampVideo = supportsTimestampedVideoPipeline();
118384
+ const canCanvasVideo = typeof HTMLCanvasElement.prototype.captureStream === "function";
118385
+ if (!navigator.mediaDevices?.getDisplayMedia || !globalThis.MediaRecorder || !canTimestampVideo && !canCanvasVideo) {
118242
118386
  showActionToast("Table recording is not supported in this browser", "fold");
118243
118387
  return;
118244
118388
  }
@@ -118249,12 +118393,15 @@ async function startTableRecording() {
118249
118393
  captureStream = await navigator.mediaDevices.getDisplayMedia({
118250
118394
  video: {
118251
118395
  displaySurface: "browser",
118252
- frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate }
118396
+ frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate },
118397
+ width: { ideal: TABLE_RECORDING_CONFIG.maxWidth },
118398
+ height: { ideal: TABLE_RECORDING_CONFIG.maxHeight }
118253
118399
  },
118254
- audio: false,
118400
+ audio: true,
118255
118401
  preferCurrentTab: true,
118256
118402
  selfBrowserSurface: "include",
118257
- surfaceSwitching: "exclude"
118403
+ surfaceSwitching: "exclude",
118404
+ systemAudio: "exclude"
118258
118405
  });
118259
118406
  const captureTrack = captureStream.getVideoTracks()[0];
118260
118407
  if (!captureTrack) throw new Error("No video track was shared");
@@ -118262,48 +118409,74 @@ async function startTableRecording() {
118262
118409
  if (displaySurface && displaySurface !== "browser") {
118263
118410
  throw new Error("Please share this browser tab, not a window or the entire screen.");
118264
118411
  }
118412
+ try {
118413
+ captureTrack.contentHint = "detail";
118414
+ } catch {
118415
+ }
118265
118416
  try {
118266
118417
  await captureTrack.applyConstraints?.({
118267
118418
  frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate }
118268
118419
  });
118269
118420
  } catch {
118270
118421
  }
118422
+ const audioTrack = captureStream.getAudioTracks()[0] || null;
118423
+ if (audioTrack?.applyConstraints) {
118424
+ try {
118425
+ await audioTrack.applyConstraints({ suppressLocalAudioPlayback: false });
118426
+ } catch {
118427
+ }
118428
+ }
118271
118429
  captureVideo = createCaptureVideo(captureStream);
118272
118430
  await captureVideo.play().catch(() => {
118273
118431
  });
118274
118432
  await waitForCaptureVideo(captureVideo, captureTrack);
118275
- const { canvas, ctx } = createTableRecordingCanvas(captureVideo);
118276
- const canvasCapture = createCanvasRecordingStream(canvas);
118277
- const { stream: canvasStream, track: canvasTrack } = canvasCapture;
118278
- try {
118279
- canvasTrack.contentHint = "detail";
118280
- } catch {
118281
- }
118282
- const recordingAudio = recordingAudioTrack();
118283
- const audioTrack = recordingAudio?.track || null;
118284
- const recorderStream = audioTrack ? new MediaStream([canvasTrack, audioTrack]) : canvasStream;
118433
+ const sourceWidth = Math.max(2, captureVideo.videoWidth || innerWidth || 2);
118434
+ const sourceHeight = Math.max(2, captureVideo.videoHeight || innerHeight || 2);
118435
+ const { canvas, ctx } = createTableRecordingCanvas(sourceWidth, sourceHeight);
118285
118436
  recording = {
118286
118437
  recorder: null,
118438
+ recorderStream: null,
118287
118439
  captureStream,
118288
118440
  captureTrack,
118289
118441
  captureVideo,
118290
118442
  canvas,
118291
118443
  ctx,
118292
- canvasStream,
118293
- canvasTrack,
118444
+ canvasStream: null,
118445
+ outputVideoTrack: null,
118446
+ processorReader: null,
118447
+ generatorWriter: null,
118448
+ videoPumpPromise: null,
118449
+ animationFrameId: null,
118450
+ sourceWidth,
118451
+ sourceHeight,
118294
118452
  audioTrack,
118295
- audioDestination: recordingAudio?.destination || null,
118296
118453
  chunks: [],
118297
118454
  stopping: false,
118298
- animationFrameId: null
118455
+ pipelineError: null
118299
118456
  };
118300
- const mimeType = recorderMimeType();
118457
+ let outputVideoTrack = canTimestampVideo ? createTimestampedVideoPipeline(recording) : null;
118458
+ if (outputVideoTrack) {
118459
+ captureVideo.pause();
118460
+ captureVideo.srcObject = null;
118461
+ captureVideo.remove();
118462
+ captureVideo = null;
118463
+ recording.captureVideo = null;
118464
+ } else {
118465
+ if (!canCanvasVideo) throw new Error("This browser cannot create a recording video track");
118466
+ outputVideoTrack = createFallbackCanvasPipeline(recording);
118467
+ }
118468
+ const tracks = [outputVideoTrack];
118469
+ if (audioTrack) tracks.push(audioTrack);
118470
+ const recorderStream = new MediaStream(tracks);
118471
+ recording.recorderStream = recorderStream;
118472
+ const mimeType = recorderMimeType(Boolean(audioTrack));
118301
118473
  const videoBitsPerSecond = recordingVideoBitrate({
118302
118474
  width: canvas.width,
118303
118475
  height: canvas.height,
118304
118476
  frameRate: TABLE_RECORDING_CONFIG.frameRate
118305
118477
  });
118306
- const options = { videoBitsPerSecond, audioBitsPerSecond: TABLE_RECORDING_CONFIG.audioBitsPerSecond };
118478
+ const options = { videoBitsPerSecond };
118479
+ if (audioTrack) options.audioBitsPerSecond = TABLE_RECORDING_CONFIG.audioBitsPerSecond;
118307
118480
  if (mimeType) options.mimeType = mimeType;
118308
118481
  const recorder = new MediaRecorder(recorderStream, options);
118309
118482
  recording.recorder = recorder;
@@ -118314,29 +118487,21 @@ async function startTableRecording() {
118314
118487
  recorder.addEventListener("stop", () => finalizeTableRecording(recording, mimeType), { once: true });
118315
118488
  recorder.addEventListener("error", (event) => {
118316
118489
  if (tableRecording !== recording) return;
118490
+ recording.pipelineError = event.error || new Error("Recording failed");
118317
118491
  recording.stopping = true;
118318
- stopTableRecordingPainter(recording);
118319
- showActionToast(summarizeError(event.error || new Error("Recording failed")), "fold");
118492
+ stopTableRecordingVideo(recording);
118320
118493
  if (recorder.state !== "inactive") recorder.stop();
118321
- else {
118322
- disposeTableRecording(recording);
118323
- tableRecording = null;
118324
- setRecordButton(false);
118325
- }
118494
+ else finalizeTableRecording(recording, mimeType);
118326
118495
  });
118327
118496
  captureTrack.addEventListener("ended", () => {
118328
- if (tableRecording === recording && recorder.state !== "inactive") {
118329
- recording.stopping = true;
118330
- stopTableRecordingPainter(recording);
118331
- recorder.stop();
118332
- }
118497
+ if (tableRecording === recording && recorder.state !== "inactive") stopTableRecording();
118333
118498
  }, { once: true });
118334
- startTableRecordingPainter(recording);
118335
- recorder.start(1e3);
118499
+ recorder.start(500);
118500
+ startTableRecordingVideo(recording);
118336
118501
  setRecordButton(true);
118337
118502
  showActionToast(
118338
- `Recording \xB7 ${canvas.width}\xD7${canvas.height} \xB7 ${TABLE_RECORDING_CONFIG.frameRate} fps${audioTrack ? " \xB7 audio" : ""}`,
118339
- "check"
118503
+ `Recording \xB7 ${canvas.width}\xD7${canvas.height} \xB7 ${TABLE_RECORDING_CONFIG.frameRate} fps \xB7 ${audioTrack ? "tab audio" : "no tab audio"}`,
118504
+ audioTrack ? "check" : "fold"
118340
118505
  );
118341
118506
  } catch (err) {
118342
118507
  if (recording) disposeTableRecording(recording);
@@ -118356,14 +118521,16 @@ function stopTableRecording() {
118356
118521
  const recording = tableRecording;
118357
118522
  if (!recording || recording.stopping) return;
118358
118523
  recording.stopping = true;
118359
- stopTableRecordingPainter(recording);
118524
+ stopTableRecordingVideo(recording);
118360
118525
  if (recording.recorder.state !== "inactive") {
118526
+ try {
118527
+ recording.recorder.requestData?.();
118528
+ } catch {
118529
+ }
118361
118530
  recording.recorder.stop();
118362
118531
  return;
118363
118532
  }
118364
- disposeTableRecording(recording);
118365
- if (tableRecording === recording) tableRecording = null;
118366
- setRecordButton(false);
118533
+ finalizeTableRecording(recording, recording.recorder.mimeType);
118367
118534
  }
118368
118535
  function modelForPlayerId(playerId) {
118369
118536
  return currentState?.config?.players?.find?.((p) => p.id === playerId)?.model || "";
@@ -118786,7 +118953,7 @@ els.clearTestsBtn.addEventListener("click", () => {
118786
118953
  });
118787
118954
  els.setupBtn.addEventListener("click", openSetup);
118788
118955
  els.closeSetup.addEventListener("click", () => els.setupDialog.close());
118789
- els.addConnectionBtn.addEventListener("click", () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind: "openai", baseUrl: "https://api.openai.com/v1" }));
118956
+ els.addConnectionBtn.addEventListener("click", () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind: ARENA_CONFIG.connections.defaultKind, baseUrl: connectionPresetUrl(ARENA_CONFIG.connections.defaultKind) }));
118790
118957
  els.pauseBtn.addEventListener("click", () => {
118791
118958
  if (!director) return;
118792
118959
  currentState?.status === "PAUSED" ? director.resume() : director.pause();
@@ -118990,7 +119157,6 @@ loadPokerTools().catch((err) => {
118990
119157
  els.setupError.classList.remove("hidden");
118991
119158
  openSetup({ preserveError: true });
118992
119159
  });
118993
- if (!storageGet("pokertoolsArenaBrowserSeen")) storageSet("pokertoolsArenaBrowserSeen", "1");
118994
119160
  var resizeTimer = null;
118995
119161
  function relayoutForViewport() {
118996
119162
  clearTimeout(resizeTimer);