← Files Codex ReplayARCHIVED FILE

mcp/controller-ranges.js

42 KB · Oct 2, 2026 · 00:19 UTC

↓ Download file

"use strict";

// Range selection shares the controller state and its existing inspect/prepare/start flow.
globalThis.createReplayRangeUI = function (context) {
  const {
    state, isObject, asArray, text,
    stringList, normalizeReviewDraft, escapeHtml, terminalRunStatus,
    render, callTool, initializeClassifications, reviewDraftFromConfiguration,
    inspectionParts, setSelectedModels, synthesizePrompt, inferWorkingDirectory,
    saveDraft, prepareRun, reviewProblems, preparationBlockers, replayDetailsLoading, selectionNeedsRefresh,
    activateRun, rememberActiveRun, preparationReady, startRun, beginPolling
  } = context;
  let inspectedUnitKey = "", inspectedCarry = "", launchingChunks = false;
  let preparedChunks = [];

  function normalizeUnitDraft(value) {
    if (!isObject(value)) return null;
    const reviewDraft = normalizeReviewDraft(value.reviewDraft);
    // Models belong to the session, including when reading older per-chunk drafts.
    if (reviewDraft) { delete reviewDraft.model; delete reviewDraft.models; }
    return {
      reviewDraft,
      classifications: Object.fromEntries(Object.entries(isObject(value.classifications) ? value.classifications : {})
        .filter(([, choice]) => ["claude", "exclude"].includes(choice))),
      attributionEdits: stringList(value.attributionEdits),
      promptGeneration: text(value.promptGeneration),
      detailsOpen: value.detailsOpen === true,
      configured: value.configured === true,
      problems: stringList(value.problems),
      carry: text(value.carry),
    };
  }

  function rememberUnitDraft() {
    if (!state.reviewDraft) return;
    const unit = activeUnit();
    const saved = unit && state.unitDrafts[unit.key];
    const inspected = unit && state.inspection && inspectedUnitKey === unit.key;
    const problems = inspected && !replayDetailsLoading()
      ? [...reviewProblems(), ...preparationBlockers(), ...(state.error ? [state.error] : [])]
      : saved?.problems || [];
    const draft = normalizeUnitDraft({
      reviewDraft: state.reviewDraft,
      classifications: state.classifications,
      attributionEdits: state.attributionEdits,
      promptGeneration: state.promptGeneration,
      detailsOpen: state.configurationDetailsOpen,
      configured: inspected
        ? !replayDetailsLoading() && !selectionNeedsRefresh() && !problems.length
        : saved?.configured,
      problems,
      carry: inspected ? inspectedCarry : saved?.carry,
    });
    if (state.replayMode === "chunks" && state.activeUnitKey) {
      state.unitDrafts[state.activeUnitKey] = draft;
    } else if (state.replayMode === "whole") {
      state.wholeDraft = draft;
    }
  }

  function restoreUnitState(draft) {
    preparedChunks = [];
    if (!draft) return;
    for (const key of ["replayMode", "actionableTurns", "dividers",
      "activeUnitKey", "unitDrafts", "wholeDraft", "pendingUnitKeys", "unitRuns", "submittingUnitKey"]) {
      state[key] = draft[key];
    }
    if (state.replayMode === "chunks") {
      state.selectedThreadId = draft.selectedThreadId;
      state.selectedThreadNumber = draft.selectedThreadNumber;
    }
  }

  function replayUnits() {
    const units = [];
    for (const [index, turn] of state.actionableTurns.entries()) {
      if (!units.length || state.dividers.includes(turn.message_uuid)) {
        units.push({ first: index, last: index, turns: [] });
      }
      const unit = units.at(-1);
      unit.last = index;
      unit.turns.push(turn);
    }
    return units.map((unit, index) => ({
      ...unit,
      number: index + 1,
      start_message_uuid: unit.turns[0].message_uuid,
      end_message_uuid: unit.turns.at(-1).message_uuid,
      key: `${unit.turns[0].message_uuid}/${unit.turns.at(-1).message_uuid}`,
    }));
  }

  function activeUnit() {
    return state.replayMode === "chunks"
      ? replayUnits().find((unit) => unit.key === state.activeUnitKey)
      : null;
  }

  function rangeParameters() {
    const unit = activeUnit();
    return unit ? {
      start_message_uuid: unit.start_message_uuid,
      end_message_uuid: unit.end_message_uuid,
    } : {};
  }

  function earlierFileAttribution(unit = activeUnit()) {
    const included = new Set(), excluded = new Set();
    if (unit) for (const earlier of replayUnits().filter((item) => item.last < unit.first)) {
      const saved = state.unitDrafts[earlier.key];
      if (!saved) continue;
      for (const [path, choice] of Object.entries(saved.classifications)) {
        if (!saved.attributionEdits.includes(path)) continue;
        if (choice === "claude") { included.add(path); excluded.delete(path); }
        else { included.delete(path); excluded.add(path); }
      }
    }
    return { carried_forward_files: [...included], excluded_files: [...excluded] };
  }

  function carryFingerprint(unit) {
    const earlier = earlierFileAttribution(unit);
    return JSON.stringify([earlier.carried_forward_files.sort(), earlier.excluded_files.sort()]);
  }

  function unitConfigured(unit) {
    const saved = state.unitDrafts[unit.key];
    return Boolean(saved?.configured && saved.carry === carryFingerprint(unit));
  }

  function allUnitsConfigured() {
    rememberUnitDraft();
    const units = replayUnits();
    return units.length > 0 && units.every(unitConfigured);
  }

  function markUnitInspected(payload) {
    const unit = activeUnit(), baseline = payload?.baseline, selection = payload?.file_selection;
    if (!unit || !isObject(baseline) || !isObject(selection) ||
      payload.replay?.message_uuid !== unit.start_message_uuid ||
      !( ["git_commit", "empty_directory"].includes(baseline.kind) ||
        (baseline.kind === "unclassified_directory" && baseline.beginning_kind === "non_git" && baseline.proposed_kind === "empty_directory")) ||
      !["git", "non_git"].includes(baseline.beginning_kind) ||
      !["git", "non_git"].includes(baseline.ending_kind) ||
      selection.source_kind !== baseline.ending_kind || !Array.isArray(selection.candidates) ||
      asArray(payload.diagnostics).some((item) => ["thread", "baseline"].includes(item?.step))) return false;
    inspectedUnitKey = unit.key;
    inspectedCarry = carryFingerprint(unit);
    return true;
  }

  function savedStateParameters(saved) {
    const draft = saved?.reviewDraft;
    if (!draft?.repo || !draft.beginning_kind || !draft.ending_kind ||
      (draft.beginning_kind === "git" && (draft.ending_kind !== "git" || !draft.baseline_commit)) ||
      (draft.ending_kind === "git" && !draft.ending_commit)) return {};
    return {
      repo: draft.repo, beginning_kind: draft.beginning_kind, ending_kind: draft.ending_kind,
      ...(draft.beginning_kind === "git" ? { baseline_commit: draft.baseline_commit } : {}),
      ...(draft.ending_kind === "git" ? { ending_commit: draft.ending_commit } : {}),
    };
  }

  function configurationIsLocked() {
    return state.batchPreparing || launchingChunks || state.pendingUnitKeys.length > 0 || Boolean(state.run) && (state.replayMode !== "chunks" ||
      state.runs.some((entry) => !terminalRunStatus(entry.run?.status)));
  }

  async function editReplayUnit(key, mode = "chunks", forLaunch = false) {
    if (state.busy || (!forLaunch && (launchingChunks || configurationIsLocked()))) return false;
    rememberUnitDraft();
    state.replayMode = mode;
    if (mode === "chunks") state.activeUnitKey = key;
    const unit = activeUnit();
    if (mode === "chunks" && !unit) return false;
    const saved = mode === "chunks" ? state.unitDrafts[key] : state.wholeDraft;
    inspectedUnitKey = "";
    const threadIdValue = state.selectedThreadId;
    state.busy = "inspection";
    state.error = "";
    state.notice = "";
    state.reviewDraft = null;
    state.inspection = null;
    state.classifications = { ...saved?.classifications };
    state.promptGeneration = saved?.promptGeneration || "";
    state.preparation = null;
    state.reviewRevision += 1;
    state.promptSynthesisGeneration += 1;
    state.workingDirectoryGeneration += 1;
    state.promptEditRevision = 0;
    state.workingDirectoryEditRevision = 0;
    state.workingDirectoryLoading = false;
    state.attributionEdits = saved?.attributionEdits || [];
    state.configurationDetailsOpen = saved?.detailsOpen ?? mode === "chunks";
    state.step = "configure";
    render();
    try {
      const payload = await callTool("inspect_thread", {
        thread_id: threadIdValue, ...rangeParameters(),
        ...(unit ? { ...earlierFileAttribution(), ...savedStateParameters(saved) } : {}),
      });
      state.inspection = payload;
      const turns = asArray(payload.replay?.actionable_user_turns || payload.thread?.actionable_user_turns);
      if (turns.length) state.actionableTurns = turns;
      initializeClassifications();
      state.reviewDraft = reviewDraftFromConfiguration();
      if (saved?.reviewDraft) {
        state.reviewDraft = normalizeReviewDraft({ ...state.reviewDraft, ...saved.reviewDraft,
          request: mode === "whole" ? saved.reviewDraft.request || state.reviewDraft.request : saved.reviewDraft.request,
          message_uuid: unit ? unit.start_message_uuid : saved.reviewDraft.message_uuid });
        for (const file of inspectionParts().files) {
          if (file.selectable !== false && saved.classifications[file.path] &&
            (!unit || saved.attributionEdits.includes(file.path))) {
            state.classifications[file.path] = saved.classifications[file.path];
          }
        }
      }
      setSelectedModels(state.selectedModels);
      state.promptGeneration = saved?.promptGeneration || text(
        payload.replay?.request_generation?.method || payload.thread?.request_generation?.method,
        "concatenated_fallback"
      );
      await Promise.all([
        state.promptGeneration === "pending"
          ? synthesizePrompt(threadIdValue, state.reviewDraft.request) : Promise.resolve(),
        !saved?.reviewDraft?.repo ? inferWorkingDirectory(threadIdValue) : Promise.resolve(),
      ]);
      markUnitInspected(payload);
      return true;
    } catch (error) {
      state.error = error instanceof Error ? error.message : String(error);
      state.inspection = null;
      state.reviewDraft = normalizeReviewDraft(saved?.reviewDraft);
      setSelectedModels(state.selectedModels);
      state.classifications = { ...saved?.classifications };
      state.promptGeneration = saved?.promptGeneration || "";
      return false;
    } finally {
      state.busy = "";
      render();
    }
  }

  async function changeReplayMode(mode) {
    if (mode === state.replayMode || state.busy || state.run) return;
    if (mode === "chunks" && state.selectedThreadId.startsWith("claude-sample:")) return;
    const units = replayUnits();
    if (mode === "chunks" && !units.length) return;
    await editReplayUnit(units.find((unit) => unit.key === state.activeUnitKey)?.key || units[0]?.key || "", mode);
  }

  async function toggleDivider(messageUuid) {
    if (state.busy || state.run || !state.actionableTurns.slice(1).some((turn) => turn.message_uuid === messageUuid)) return;
    rememberUnitDraft();
    state.dividers = state.dividers.includes(messageUuid)
      ? state.dividers.filter((id) => id !== messageUuid) : [...state.dividers, messageUuid];
    const units = replayUnits();
    state.activeUnitKey = "";
    state.reviewDraft = null;
    await editReplayUnit(units[0].key);
  }

  function remainingUnits() {
    return replayUnits().filter((unit) => !state.unitRuns[unit.key]?.length);
  }

  async function startChunks() {
    if (launchingChunks || state.busy || configurationIsLocked() || state.submittingUnitKey) return;
    rememberUnitDraft();
    preparedChunks = [];
    state.pendingUnitKeys = remainingUnits().map((unit) => unit.key);
    await advanceChunkQueue();
  }

  function chunkModelCount() {
    return state.selectedModels.length;
  }

  function chunkFitsCapacity() {
    return state.runs.filter((entry) => !terminalRunStatus(entry.run?.status)).length +
      chunkModelCount() <= state.maxParallelRuns;
  }

  async function advanceChunkQueue() {
    if (launchingChunks || state.busy || state.submittingUnitKey) return false;
    state.pendingUnitKeys = state.pendingUnitKeys.filter((key) => !state.unitRuns[key]?.length);
    preparedChunks = preparedChunks.filter(({key}) => state.pendingUnitKeys.includes(key));
    saveDraft();
    if (!state.pendingUnitKeys.length) return false;
    if (!chunkModelCount() || chunkModelCount() > state.maxParallelRuns) {
      state.error = `Choose between 1 and ${state.maxParallelRuns} models for this replay.`;
      state.pendingUnitKeys = [];
      preparedChunks = [];
      render();
      return true;
    }
    if (!chunkFitsCapacity()) return false;
    launchingChunks = true;
    try {
      if (!preparedChunks.length) {
        const prepared = [];
        for (const key of state.pendingUnitKeys) {
          if (!await editReplayUnit(key, "chunks", true)) { pauseChunkQueue(); return true; }
          await prepareRun({ autoStart: false });
          if (!preparationReady()) { pauseChunkQueue(); return true; }
          rememberUnitDraft();
          if (!unitConfigured(activeUnit())) { pauseChunkQueue(); return true; }
          prepared.push({key, inspection: state.inspection,
            draft: normalizeUnitDraft(state.unitDrafts[key])});
        }
        preparedChunks = prepared;
      }
      if (!allUnitsConfigured()) {
        state.pendingUnitKeys = [];
        state.notice = "A chunk's historical state changed. Start again to recheck the chunks.";
        return true;
      }
      for (const {key, inspection, draft} of preparedChunks) {
        if (!state.pendingUnitKeys.includes(key)) continue;
        if (!chunkFitsCapacity()) break;
        state.activeUnitKey = key;
        state.inspection = inspection;
        state.preparation = null;
        state.step = "configure";
        state.reviewDraft = normalizeReviewDraft(draft.reviewDraft);
        state.classifications = { ...draft.classifications };
        state.attributionEdits = [...draft.attributionEdits];
        state.promptGeneration = draft.promptGeneration;
        setSelectedModels(state.selectedModels);
        markUnitInspected(inspection);
        await prepareRun({ autoStart: false });
        if (!preparationReady()) { pauseChunkQueue(); break; }
        await startRun();
        if (!state.unitRuns[key]?.length) { state.pendingUnitKeys = []; break; }
        state.pendingUnitKeys = state.pendingUnitKeys.filter((value) => value !== key);
        saveDraft();
      }
    } finally {
      launchingChunks = false;
      if (!state.pendingUnitKeys.length) preparedChunks = [];
      render({ focus: true });
      if (state.runs.some((entry) => !terminalRunStatus(entry.run?.status))) beginPolling();
    }
    return true;
  }

  function chunkLaunchActive() { return launchingChunks; }

  function pauseChunkQueue() {
    state.pendingUnitKeys = [];
    state.configurationDetailsOpen = true;
    state.notice = `Chunk ${activeUnit()?.number} needs attention. Resolve the issue below, then start the remaining chunks.`;
  }

  function renderPendingChunks() {
    const count = state.pendingUnitKeys.length;
    return count ? `<p class="chunk-help">${count} ${count === 1 ? "chunk" : "chunks"} queued. Up to ${state.maxParallelRuns} model runs can run at a time.</p>` : "";
  }

  function renderRangeSelection() {
    rememberUnitDraft();
    const split = state.replayMode === "chunks";
    const disabled = state.busy || configurationIsLocked();
    const units = replayUnits();
    return `<section class="panel range-selection" aria-labelledby="replay-scope-heading">
      <div class="panel__header"><div><h2 id="replay-scope-heading">Replay scope</h2>
        <p>Replay the whole thread or choose ranges of user turns.</p></div></div>
      <div class="panel__body">
        <div class="variant-options" role="group" aria-label="Replay scope">
          <button type="button" class="button button--quiet" data-action="replay-mode" data-mode="whole" aria-pressed="${!split}" ${disabled || state.run ? "disabled" : ""}>Whole thread</button>
          <button type="button" class="button button--quiet" data-action="replay-mode" data-mode="chunks" aria-pressed="${split}" ${disabled || state.run || !units.length || state.selectedThreadId.startsWith("claude-sample:") ? "disabled" : ""}>Split into chunks</button>
        </div>
        ${split ? `<p class="chunk-help">Add dividers between user turns. Start checks every chunk automatically and asks for help only when a setting needs attention. Up to ${state.maxParallelRuns} model runs can run at a time.</p>
          <div class="chunk-timeline">${state.actionableTurns.map((turn, index) => `${index ? `<button type="button" class="chunk-divider" data-action="toggle-divider" data-message-uuid="${escapeHtml(turn.message_uuid)}" aria-pressed="${state.dividers.includes(turn.message_uuid)}" ${disabled || state.run ? "disabled" : ""}><span>${state.dividers.includes(turn.message_uuid) ? "Remove divider" : "Add divider"}</span></button>` : ""}
            <div class="chunk-turn"><span>${index + 1}</span><p>${escapeHtml(text(turn.request, "User turn").slice(0, 320))}</p></div>`).join("")}</div>
          <div class="chunk-choices" role="group" aria-label="Chunks to replay">${units.map((unit) => `<div class="chunk-choice${unit.key === state.activeUnitKey ? " chunk-choice--active" : ""}">
            <div>Chunk ${unit.number}<small> · Turns ${unit.first + 1}–${unit.last + 1} · ${unitConfigured(unit) ? "Ready" : state.unitDrafts[unit.key]?.problems?.length ? "Needs attention" : "Checks on start"}</small></div>
            <button type="button" class="button button--quiet" data-action="edit-unit" data-unit-key="${escapeHtml(unit.key)}" ${disabled ? "disabled" : ""}>${unit.key === state.activeUnitKey ? "Editing" : "Configure"}</button>
          </div>`).join("")}</div>
          <p class="chunk-help">The selected models apply to every chunk.</p>` : ""}
      </div>
    </section>`;
  }

  function recoverRangeRuns(active) {
    if (active.start_message_uuid && active.end_message_uuid) {
      state.replayMode = "chunks";
      state.selectedThreadId = text(active.thread_id);
    }
    return state.replayMode === "chunks";
  }

  function resultUnits(records = state.runs.map((entry) => entry.run)) {
    if (state.replayMode !== "chunks") return [];
    const ranges = records.filter((run) => run.start_message_uuid && run.end_message_uuid &&
      (!state.selectedThreadId || run.thread_id === state.selectedThreadId));
    const configured = replayUnits();
    if (configured.length && ranges.every((run) => configured.some((unit) =>
      unit.start_message_uuid === run.start_message_uuid && unit.end_message_uuid === run.end_message_uuid))) return configured;
    const units = new Map();
    for (const run of ranges.sort((a, b) =>
      text(a.started_at || a.created_at || a.run_id).localeCompare(text(b.started_at || b.created_at || b.run_id)) ||
      text(a.run_id).localeCompare(text(b.run_id)))) {
      const key = `${run.start_message_uuid}/${run.end_message_uuid}`;
      if (!units.has(key)) units.set(key, {key, number: units.size + 1,
        start_message_uuid: run.start_message_uuid, end_message_uuid: run.end_message_uuid});
    }
    return [...units.values()];
  }

  function resultUnit() {
    if (state.replayMode !== "chunks") return null;
    const entry = state.runs.find((item) => item.id === state.runId);
    return resultUnits().find((unit) => unit.number === entry?.unitNumber) || null;
  }

  function resultScope() {
    const unit = resultUnit();
    return unit ? `Chunk ${unit.number}${Number.isInteger(unit.first) ? ` · Turns ${unit.first + 1}–${unit.last + 1}` : ""}` : "";
  }

  function selectChunkResult(number) {
    const entries = state.runs.filter((entry) => entry.unitNumber === number);
    const current = state.runs.find((entry) => entry.id === state.runId);
    const entry = entries.find((item) => item.report && item.model === current?.model)
      || entries.find((item) => item.report) || entries[0];
    if (!entry) return;
    activateRun(entry.id);
    rememberActiveRun(entry.id);
    state.step = "results";
    render({ focus: true });
  }

  function renderChunkResults() {
    if (state.replayMode !== "chunks") return "";
    const selected = resultUnit();
    return `<section class="panel chunk-results" aria-labelledby="chunk-results-heading">
      <div class="panel__header"><div><h2 id="chunk-results-heading">Results by chunk</h2>
        <p>Each chunk has its own comparison report and judge results.</p></div></div>
      <nav class="panel__body chunk-result-options" aria-label="Chunk results">${resultUnits().map((unit) => {
        const entries = state.runs.filter((entry) => entry.unitNumber === unit.number);
        const reports = entries.filter((entry) => entry.report).length;
        const status = reports ? `${reports} ${reports === 1 ? "report" : "reports"}`
          : entries.some((entry) => !terminalRunStatus(entry.run?.status)) ? "Running"
            : entries.length ? "No report" : "Waiting";
        return `<button type="button" class="button button--quiet" data-action="select-chunk-result" data-unit-number="${unit.number}" aria-pressed="${selected?.number === unit.number}" ${entries.length ? "" : "disabled"}>
          <strong>Chunk ${unit.number}</strong>${Number.isInteger(unit.first) ? `<span>Turns ${unit.first + 1}–${unit.last + 1}</span>` : ""}<small>${status}</small></button>`;
      }).join("")}</nav>
    </section>`;
  }

  return {
    normalizeUnitDraft, rememberUnitDraft, restoreUnitState, replayUnits,
    activeUnit, rangeParameters, earlierFileAttribution, configurationIsLocked,
    editReplayUnit, changeReplayMode, toggleDivider, remainingUnits, unitConfigured, allUnitsConfigured, markUnitInspected,
    startChunks, advanceChunkQueue, chunkLaunchActive, renderPendingChunks, renderRangeSelection, resultUnit, selectChunkResult, renderChunkResults,
    recoverRangeRuns, resultUnits, resultScope
  };
};

// Thread batches share the same configuration and individual report surfaces.
globalThis.createReplayBatchUI = function (context) {
  const {
    state, text, isObject, asArray, stringList, normalizeUnitDraft, normalizeReviewDraft, threadTitle, threadId, reviewProblems, preparationBlockers, replayDetailsLoading, selectionNeedsRefresh, render, selectThread, setSelectedModels, refreshAttributionAndContinue, reviewPayload, callTool, ownedRunRecord, currentRunId, clearDraft, rememberActiveRun, activateRun, beginPolling, applyPreparationInspection, formatCost, formatDate, terminalRunStatus, successfulRunStatus, pageHeading, escapeHtml, setStartPending, clearRestoredDraft
  } = context;
  function normalizeSelectedThreads(values) {
    const seen = new Set();
    return asArray(values).flatMap((entry) => {
      const id = text(entry?.id);
      if (!id || seen.has(id)) return [];
      seen.add(id);
      return [{ id, number: Number(entry.number) || seen.size,
        thread: { imported_thread_id: id, title: threadTitle(entry.thread),
          source_path: text(entry.thread?.source_path), project_dir: text(entry.thread?.project_dir) },
        draft: normalizeUnitDraft(entry.draft) }];
    });
  }

  function isThreadBatch() {
    return Boolean(state.batchId) || state.selectedThreads.length > 1;
  }

  function restoreBatch(payload) {
    const batch = payload?.batch;
    if (!isObject(batch) || !text(batch.id)) return false;
    state.batchId = text(batch.id);
    state.batch = batch;
    state.replayMode = "whole";
    state.runErrors = asArray(batch.errors).filter(isObject);
    if (asArray(batch.models).length) setSelectedModels(batch.models);
    if (asArray(batch.threads).length) state.selectedThreads = normalizeSelectedThreads(batch.threads.map((entry, index) => ({
      id: entry.thread_id, number: index + 1, thread: { title: entry.thread_title },
    })));
    return true;
  }

  function rememberBatchThread() {
    if (!isThreadBatch() || state.replayMode !== "whole" || state.run || !state.reviewDraft || state.busy === "inspection") return;
    const entry = state.selectedThreads.find((item) => item.id === state.selectedThreadId);
    if (!entry || state.reviewDraft.thread_id !== entry.id) return;
    entry.draft = normalizeUnitDraft({ reviewDraft: state.reviewDraft,
      classifications: state.classifications, attributionEdits: state.attributionEdits,
      promptGeneration: state.promptGeneration, detailsOpen: state.configurationDetailsOpen,
      problems: [...reviewProblems(), ...preparationBlockers(), ...(state.error ? [state.error] : [])],
      configured: !replayDetailsLoading() && !selectionNeedsRefresh() &&
        !reviewProblems().length && !preparationBlockers().length && !state.error });
  }

  function toggleThreadSelection(id, number) {
    if (state.busy || state.run || state.batchPreparing) return;
    rememberBatchThread();
    const selected = state.selectedThreads.find((item) => item.id === id);
    if (selected) state.selectedThreads = state.selectedThreads.filter((item) => item.id !== id);
    else if (state.selectedThreads.length < 100) {
      const thread = state.threads.find((item) => threadId(item) === id);
      if (thread) state.selectedThreads.push({ id, number, thread, draft: null });
    }
    render();
  }

  async function configureSelectedThreads() {
    if (state.busy || state.run || !state.selectedThreads.length) return;
    const entry = state.selectedThreads.find((item) => item.id === state.selectedThreadId)
      || state.selectedThreads[0];
    if (state.selectedThreads.length === 1 && entry.id === state.selectedThreadId && state.inspection) {
      state.step = "configure";
      render({ focus: true });
      return;
    }
    await selectBatchThread(entry.id);
  }

  async function selectBatchThread(id, { forStart = false } = {}) {
    if (state.busy || state.run || (state.batchPreparing && !forStart)) return;
    rememberBatchThread();
    const entry = state.selectedThreads.find((item) => item.id === id);
    if (!entry) return;
    const saved = id === state.selectedThreadId && state.replayMode === "chunks"
      ? state.wholeDraft : entry.draft;
    await selectThread(id, entry.number, { selected: entry.thread, saved });
    rememberBatchThread();
  }

  function renderBatchConfiguration() {
    rememberBatchThread();
    return `<section class="panel" aria-labelledby="selected-thread-heading">
      <div class="panel__header"><div><h2 id="selected-thread-heading">${state.selectedThreads.length} selected threads</h2>
        <p>Each thread keeps its own configuration. The selected models apply to every thread.</p></div>
        <button type="button" class="button button--quiet" data-action="configuration-back" ${state.busy || state.batchPreparing ? "disabled" : ""}>Choose threads</button></div>
      <div class="panel__body chunk-choices">${state.selectedThreads.map((entry) => `<div class="chunk-choice${entry.id === state.selectedThreadId ? " chunk-choice--active" : ""}">
        <div><strong>${escapeHtml(threadTitle(entry.thread))}</strong><small> · ${entry.draft?.problems.length ? "Needs attention" : entry.draft?.configured ? "Ready" : "Checks on start"}</small></div>
        <button type="button" class="button button--quiet" data-action="configure-thread" data-thread-id="${escapeHtml(entry.id)}" ${state.busy || state.batchPreparing ? "disabled" : ""}>${entry.id === state.selectedThreadId ? "Editing" : "Configure"}</button>
      </div>`).join("")}</div></section>`;
  }

  async function startThreadBatch() {
    if (!isThreadBatch() || state.busy || state.run || state.batchId || state.batchPreparing) return;
    rememberBatchThread();
    state.batchPreparing = true;
    let launchRequested = false;
    state.error = "";
    state.notice = "";
    try {
      const configurations = [];
      for (const entry of state.selectedThreads) {
        await selectBatchThread(entry.id, { forStart: true });
        if (selectionNeedsRefresh()) await refreshAttributionAndContinue();
        if (reviewProblems().length || replayDetailsLoading() || state.error) {
          state.configurationDetailsOpen = true;
          state.notice = `${threadTitle(entry.thread)} needs attention. Review its configuration, then start the threads again.`;
          return;
        }
        configurations.push({ ...reviewPayload(), thread_title: threadTitle(entry.thread) });
      }
      state.busy = "prepare";
      render();
      const preparation = await callTool("prepare_run", { configurations });
      state.busy = "";
      if (preparation.ready !== true && preparation.can_run !== true) {
        const blocked = asArray(preparation.preparations).find((entry) =>
          entry.preparation?.ready !== true && entry.preparation?.can_run !== true);
        if (blocked) {
          await selectBatchThread(blocked.thread_id, { forStart: true });
          state.preparation = blocked.preparation;
          applyPreparationInspection(blocked.preparation);
        } else state.preparation = preparation;
        state.configurationDetailsOpen = true;
        state.notice = "A thread needs attention. Review its configuration, then start the threads again.";
        return;
      }
      state.busy = "start";
      launchRequested = true;
      setStartPending(true);
      render();
      const payload = await callTool("start_run", {
        ...preparation.run_config, approved: true,
        prepare_token: preparation.prepare_token || preparation.approval?.prepare_token,
      });
      state.runs = asArray(payload.runs).flatMap((item) => {
        const run = ownedRunRecord(item), id = currentRunId(item);
        return run && id ? [{ id, run, model: text(item.model || run.model) }] : [];
      });
      state.runErrors = asArray(payload.errors).filter(isObject);
      restoreBatch(payload);
      state.batchId = state.batchId || text(state.runs[0]?.run.batch_id || payload.batch_id);
      if (!state.runs.length && !state.batchId) throw new Error("The controller started no durable run.");
      state.batchView = "aggregate";
      activateRun(state.runs[0]?.id);
      rememberActiveRun(state.runId);
      state.step = "run";
      clearDraft();
      clearRestoredDraft();
      beginPolling();
    } catch (error) {
      state.error = error instanceof Error ? error.message : String(error);
      if (launchRequested) {
        try {
          const payload = await callTool("get_state", {});
          const snapshot = payload.state || payload;
          if (restoreBatch(snapshot)) {
            state.step = "run";
            beginPolling();
          }
        } catch { /* Reopening the controller reconciles the durable launch. */ }
      }
    } finally {
      setStartPending(false);
      state.batchPreparing = false;
      state.busy = "";
      render({ focus: true });
    }
  }

  function batchThreadResults() {
    const threads = new Map();
    for (const entry of asArray(state.batch?.threads)) {
      threads.set(entry.thread_id, { id: entry.thread_id, title: entry.thread_title, entries: [] });
    }
    for (const entry of state.runs) {
      const id = text(entry.run.thread_id);
      if (!threads.has(id)) threads.set(id, { id, title: text(entry.run.thread_title,
        threadTitle(state.selectedThreads.find((item) => item.id === id)?.thread)), entries: [] });
      threads.get(id).entries.push(entry);
    }
    return [...threads.values()];
  }

  function selectThreadResult(id) {
    const entries = state.runs.filter((entry) => entry.run.thread_id === id);
    const current = state.runs.find((entry) => entry.id === state.runId);
    const entry = entries.find((item) => item.report && item.model === current?.model)
      || entries.find((item) => item.report);
    if (!entry) return;
    activateRun(entry.id);
    rememberActiveRun(entry.id);
    state.batchView = "individual";
    state.step = "results";
    render({ focus: true });
  }

  function renderBatchResultsNavigation() {
    if (!isThreadBatch()) return "";
    const threads = batchThreadResults();
    const finished = threads.filter(({ id, entries }) =>
      !state.batch?.starting && entries.every((entry) => terminalRunStatus(entry.run.status)) &&
      entries.length + state.runErrors.filter((error) => error.thread_id === id).length >= state.selectedModels.length).length;
    return `<section class="panel chunk-results" aria-labelledby="thread-results-heading">
      <div class="panel__header"><div><h2 id="thread-results-heading">${finished} of ${threads.length} threads finished</h2>
        <p>Completed comparisons are available while the other threads continue.</p></div></div>
      <nav class="panel__body chunk-result-options" aria-label="Thread results">
        <button type="button" class="button button--quiet" data-action="aggregate-results" aria-pressed="${state.batchView === "aggregate"}"><strong>Aggregate</strong><small>${state.runs.filter((entry) => entry.report).length} comparisons ready</small></button>
        ${threads.map(({ id, title, entries }) => {
          const reports = entries.filter((entry) => entry.report).length;
          const active = state.batchView === "individual" && state.run?.thread_id === id;
          const total = state.batch?.models?.length || state.selectedModels.length;
          const status = reports ? `${reports} of ${total} comparisons ready`
            : entries.some((entry) => !terminalRunStatus(entry.run.status)) ? "Running" : "No report";
          return `<button type="button" class="button button--quiet" data-action="thread-results" data-thread-id="${escapeHtml(id)}" aria-pressed="${active}" ${reports ? "" : "disabled"}><strong>${escapeHtml(title)}</strong><small>${status}</small></button>`;
        }).join("")}
      </nav></section>`;
  }

  function aggregateModelResults() {
    return stringList([...state.runs.map((entry) => entry.model), ...asArray(state.batch?.models)]).map((model) => {
      const entries = state.runs.filter((entry) => entry.model === model);
      const reports = entries.flatMap((entry) => entry.report ? [entry.report] : []);
      const paired = reports.filter((report) => !report.historical_usage_shared &&
        report.estimated_cost?.claude?.status !== "shared" &&
        [report.estimated_cost?.claude?.usd, report.estimated_cost?.codex?.usd]
          .every((value) => Number.isFinite(value) && value >= 0));
      const quality = { wins: 0, ties: 0, losses: 0, unavailable: 0 };
      for (const report of reports) {
        const evaluation = report.evaluation || {};
        const mapping = evaluation.candidate_mapping || { A: "claude", B: "codex" };
        const totals = evaluation.totals || {};
        if (!Number.isFinite(totals.A) || !Number.isFinite(totals.B) ||
            new Set([mapping.A, mapping.B]).size !== 2 ||
            ![mapping.A, mapping.B].every((value) => ["claude", "codex"].includes(value))) {
          quality.unavailable += 1;
          continue;
        }
        const rounded = (value) => report.schema_version === 2 ? value : Math.round(value * 100);
        const claude = rounded(totals[mapping.A === "claude" ? "A" : "B"]);
        const codex = rounded(totals[mapping.A === "codex" ? "A" : "B"]);
        quality[codex === claude ? "ties" : codex > claude ? "wins" : "losses"] += 1;
      }
      const claude = paired.reduce((sum, report) => sum + report.estimated_cost.claude.usd, 0);
      const codex = paired.reduce((sum, report) => sum + report.estimated_cost.codex.usd, 0);
      return { model, total: state.batch?.thread_count || entries.length, ready: reports.length, paired: paired.length,
        failed: entries.filter((entry) => terminalRunStatus(entry.run.status) && !successfulRunStatus(entry.run.status)).length +
          state.runErrors.filter((error) => error.model === model).length,
        claude, codex, quality };
    });
  }

  function renderAggregateResults() {
    return `${pageHeading("Comparison results", "All selected threads", "Totals update as comparisons finish. Costs are API-equivalent estimates for completed comparisons, not subscription bills.")}
      ${renderBatchResultsNavigation()}
      <div class="variant-grid">${aggregateModelResults().map((item) => {
        const model = state.models.find(({ id }) => id === item.model)?.label || item.model;
        const difference = item.paired && item.claude > 0 ? (item.claude - item.codex) / item.claude * 100 : null;
        return `<section class="panel"><div class="panel__header"><div><h2>${escapeHtml(model)}</h2>
          <p>${item.ready} of ${item.total} comparisons ready${item.failed ? ` · ${item.failed} failed or cancelled` : ""}</p></div></div>
          <div class="panel__body"><dl class="metrics">
            <div class="metric"><dt>Historical Claude</dt><dd>${item.paired ? formatCost(item.claude) : "Unavailable"}</dd></div>
            <div class="metric"><dt>Codex replay</dt><dd>${item.paired ? formatCost(item.codex) : "Unavailable"}</dd></div>
            <div class="metric"><dt>Cost difference</dt><dd>${difference === null ? "Unavailable" : `${Math.abs(difference).toFixed(1)}% ${difference >= 0 ? "lower" : "higher"}`}</dd></div>
            <div class="metric"><dt>LLM judge</dt><dd>${item.quality.wins} wins · ${item.quality.ties} ties · ${item.quality.losses} losses</dd></div>
          </dl><p class="chunk-help">Cost coverage: ${item.paired} of ${item.ready} completed comparisons. Missing or shared historical usage is excluded from both cost totals.${item.quality.unavailable ? ` ${item.quality.unavailable} judge results unavailable.` : ""}</p></div>
        </section>`;
      }).join("")}</div>`;
  }

  function renderThreadStep() {
    const threads = state.threads;
    return `
      ${pageHeading("Historical source", "Choose imported Claude threads",
        "Select one or more conversations to replay. Claude is not rerun.")}
      ${state.busy === "inspection"
        ? `<div class="banner" role="status"><span class="spinner" aria-hidden="true"></span><div class="banner__body"><strong>Inspecting ${escapeHtml(threadTitle(state.selectedThread || {}))}</strong><p>Loading the thread, models, baseline, and capabilities…</p></div></div>` : ""}
      ${state.run
        ? `<div class="banner banner--warning" role="status"><span class="banner__mark" aria-hidden="true">i</span><div class="banner__body"><strong>A replay is already ${terminalRunStatus(state.run.status) ? "recorded" : "running"}</strong><p>To run another replay, open a new Codex task and invoke Codex Replay.</p></div></div>` : ""}
      <section class="panel" aria-labelledby="threads-heading">
        <div class="panel__header">
          <div><h2 id="threads-heading">${state.threadSource === "sample" ? "Sample data" : "Imported threads"}</h2>
            <p>${state.loading ? "Loading local imports…" : `${threads.length} of ${state.totalThreads} shown`}</p></div>
          <button type="button" class="button button--quiet" data-action="refresh-threads" ${state.busy ? "disabled" : ""}>Refresh</button>
        </div>
        <div class="panel__body">
          <div class="thread-sources" aria-label="Thread source">
            ${[["imported", "Imported threads", state.importedTotal], ["sample", "Sample data", state.sampleTotal]]
              .map(([source, label, total]) => `<button type="button" class="button button--quiet" data-action="thread-source" data-source="${source}" aria-pressed="${state.threadSource === source}">${label} (${total})</button>`).join("")}
          </div>
          <label class="sr-only" for="thread-search">Search imported threads</label>
          <div class="search"><span class="search__icon" aria-hidden="true"></span><input id="thread-search" type="search" autocomplete="off" placeholder="Search by title, project, model, or ID" value="${escapeHtml(state.query)}"></div>
          ${state.loading
            ? `<div class="thread-list" aria-label="Loading threads"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>`
            : threads.length
              ? `<div class="thread-list">${threads.map((thread, index) => {
                const id = threadId(thread);
                const number = index + 1;
                const project = text(thread.project_dir, "No project detected");
                const complexity = Number(thread.complexity_score);
                const calls = Number(thread.tool_call_count);
                const checked = state.selectedThreads.some((entry) => entry.id === id);
                return `<label class="thread thread--select"><input type="checkbox" data-thread-selection="${escapeHtml(id)}" data-thread-number="${number}" aria-label="Select ${escapeHtml(threadTitle(thread))}" ${checked ? "checked" : ""} ${!id || state.busy || state.run || !checked && state.selectedThreads.length >= 100 ? "disabled" : ""}><span><span class="thread__title"><span class="thread__number">#${number}</span>${escapeHtml(threadTitle(thread))}</span><span class="thread__meta"><span>${escapeHtml(formatDate(thread.created_at))}</span><span class="thread__project" title="${escapeHtml(project)}">${escapeHtml(project)}</span>${Number.isFinite(complexity) ? `<span>Complexity ${complexity}</span>` : ""}${Number.isFinite(calls) ? `<span>${calls} tool calls</span>` : ""}${thread.claude_model ? `<span>${escapeHtml(thread.claude_model)}</span>` : ""}</span></span></label>`;
              }).join("")}</div>`
              : `<div class="empty">${state.query
                  ? "No threads match this search."
                  : state.threadSource === "sample"
                    ? "No sample data is available."
                    : "No Claude conversations imported. Import one into Codex, then select Refresh."}</div>`
          }
          ${!state.loading && state.threads.length < state.totalThreads
            ? `<button type="button" class="button button--quiet" data-action="load-more-threads" ${state.busy ? "disabled" : ""}>Load more threads</button>`
            : ""}
        </div></section>
      <div class="footer-actions"><span class="footer-actions__note">${state.selectedThreads.length} selected${state.selectedThreads.length >= 100 ? " · Maximum 100 threads" : ""}</span>
        <button type="button" class="button button--primary" data-action="configure-selected" ${state.busy || state.run || !state.selectedThreads.length ? "disabled" : ""}>Configure ${state.selectedThreads.length === 1 ? "thread" : "threads"}</button></div>
    `;
  }

  return { normalizeSelectedThreads, isThreadBatch, restoreBatch, rememberBatchThread, toggleThreadSelection, configureSelectedThreads, selectBatchThread, renderBatchConfiguration, startThreadBatch, batchThreadResults, selectThreadResult, renderBatchResultsNavigation, aggregateModelResults, renderAggregateResults, renderThreadStep };
};

SHA-256: 0f4980819a57a0e6d52cdd4f0f9507774e5d44ed66ad8afee140cf81a6c0c2d8