// render-plans.js — Plan rendering functions extracted from dashboard.html

const PLANS_PER_PAGE = 10;
let _plansPage = 0;

function _plansPrev() { if (_plansPage > 0) { _plansPage--; refresh(); } }
function _plansNext() { _plansPage++; refresh(); }

function openCreatePlanModal() {
  const projOpts = (typeof cmdProjects !== 'undefined' ? cmdProjects : []).map(p =>
    '<option value="' + escapeHtml(p) + '">' + escapeHtml(p) + '</option>'
  ).join('');
  const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';

  document.getElementById('modal-title').textContent = 'Create Plan';
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: project names)
  document.getElementById('modal-body').innerHTML =
    '<div style="display:flex;flex-direction:column;gap:10px">' +
      '<label style="color:var(--text);font-size:var(--text-md)">Title <input id="plan-new-title" style="' + inputStyle + '" placeholder="e.g. Add user authentication with JWT"></label>' +
      '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="plan-new-project" style="' + inputStyle + '"><option value="">Multiple / cross-repo (agent routes per item)</option>' + projOpts + '</select><span style="display:block;font-size:var(--text-sm);color:var(--muted);margin-top:2px">Pick a project to scope every item to one repo. Leave on cross-repo for plans that span multiple projects.</span></label>' +
      '<label style="color:var(--text);font-size:var(--text-md)">Plan Content <textarea id="plan-new-content" rows="12" style="' + inputStyle + ';resize:vertical;font-family:monospace;font-size:var(--text-md)" placeholder="Write your plan in markdown...\n\nDescribe what needs to be built, the approach, requirements, and any constraints.\n\nThe squad will convert this into a PRD with structured work items."></textarea></label>' +
      '<div style="font-size:var(--text-base);color:var(--muted)">After creating, click Execute on the plan card to have an agent convert it into a PRD with work items.</div>' +
      '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
        '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
        '<button onclick="_submitCreatePlan(event)" class="btn-primary-lg">Create Plan</button>' +
      '</div>' +
    '</div>';
  document.getElementById('modal').classList.add('open');
  setTimeout(() => document.getElementById('plan-new-title')?.focus(), 100);
}

async function _submitCreatePlan(e) {
  var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
  const title = document.getElementById('plan-new-title')?.value?.trim();
  const content = document.getElementById('plan-new-content')?.value?.trim();
  if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Title is required'); return; }
  if (!content) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Plan content is required'); return; }
  const project = document.getElementById('plan-new-project')?.value || '';

  try {
    const res = await fetch('/api/plans/create', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, content, project })
    });
    const data = await res.json();
    if (res.ok) {
      try { closeModal(); } catch { /* expected */ }
      refreshPlans();
      refresh();
      try { showToast('cmd-toast', 'Plan "' + data.file + '" created — click Execute to convert to PRD', true); } catch { /* expected */ }
    } else {
      alert('Failed: ' + (data.error || 'unknown'));
    }
  } catch (e) { alert('Error: ' + e.message); }
}

async function refreshPlans() {
  try {
    const plans = await fetch('/api/plans').then(r => r.json());
    // Cross-slice readers (renderMeetings, plan modal, command-center, etc.)
    // depend on _lastPlans staying current — assign BEFORE the change gate so
    // they see fresh data even when the renderer is skipped (F2/F3).
    window._lastPlans = plans;
    // Skip the full plans-list innerHTML rewrite when /api/plans returned the
    // same payload as last tick. Dedicated cache key 'plansPayload' avoids
    // colliding with the 'plans' slice tracked by _processStatusUpdate (F6).
    if (typeof _changed !== 'function' || _changed('plansPayload', plans)) {
      renderPlans(plans);
      // F2 (W-mpgb0xe6000gb66d): if a meeting modal is open, ask it to re-check
      // its linked-plan resolution against the freshly fetched plan list. Without
      // this hook, a meeting modal's "Create Plan from Meeting" → "View Plan"
      // flip waits for the next meeting-object change (refresh.js:135 gates
      // renderMeetings on _changed(meetings, ...)). Gated on _changed so we only
      // re-render meeting modal when plan data actually moved (F6 alignment).
      try {
        if (typeof _rerenderMeetingLinkedPlanIfChanged === 'function') {
          _rerenderMeetingLinkedPlanIfChanged();
        }
      } catch { /* hook is best-effort — never block plan refresh on it */ }
    }
  } catch (e) { console.error('plans refresh:', e.message); }
}

/**
 * Returns true when a failed plan follow-up work item's intended outcome has
 * been satisfied by a later/alternate path — so the stale failure should not
 * drive the plan's top-level "has failures" badge.
 *
 * Reconciliation rules (issue #2366):
 * - Failed `itemType: 'pr'` (Create PR for plan): satisfied when a PR linked
 *   to the same plan exists in any non-terminated state (active/merged/linked).
 *   The original failure is preserved on the WI for audit; it just doesn't
 *   count toward the rollup once a PR actually got created.
 * - Failed `itemType: 'verify'`: satisfied when a newer verify WI for the
 *   same plan + project exists and is done OR in-flight (pending/dispatched).
 *
 * Returns false when the outcome cannot be confirmed satisfied — failure
 * remains live and drives the badge.
 */
function isFollowUpOutcomeSatisfied(wi, allWorkItems, allPrs) {
  if (!wi || wi.status !== 'failed') return false;
  if (wi.itemType === 'pr') {
    // PR for this plan was created on any non-terminated path.
    // "abandoned" / "closed" are terminal failures; everything else counts as
    // the desired outcome being reached.
    return (allPrs || []).some(pr =>
      pr && pr.sourcePlan === wi.sourcePlan &&
      pr.status !== 'abandoned' && pr.status !== 'closed'
    );
  }
  if (wi.itemType === 'verify') {
    // A newer verify WI for the same plan + project that is in flight or done.
    const wiCreated = wi.created || '';
    return (allWorkItems || []).some(other =>
      other && other !== wi && other.itemType === 'verify' &&
      other.sourcePlan === wi.sourcePlan &&
      (other.project || '') === (wi.project || '') &&
      (other.status === 'done' || other.status === 'pending' || other.status === 'dispatched') &&
      (other.created || '') > wiCreated
    );
  }
  return false;
}

/**
 * Derive effective plan/PRD status from work items (single source of truth).
 * PRD JSON status is treated as user intent (approved, paused, rejected),
 * but completion/progress is always derived from actual work item state.
 *
 * opts.allPrs — optional PR records used to reconcile stale failed follow-up
 * tasks (issue #2366). Falls back to window._lastPullRequests in the browser.
 * Pass `[]` to disable reconciliation.
 */
function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems, opts) {
  const allPrs = (opts && opts.allPrs)
    || (typeof window !== 'undefined' && window._lastPullRequests)
    || [];
  const wi = workItems.filter(w =>
    w.sourcePlan === prdFile || w.sourcePlan === mdFile ||
    (w.type === 'plan-to-prd' && (w.planFile === prdFile || w.planFile === mdFile))
  );
  const implementWi = wi.filter(w => w.type !== 'plan-to-prd' && w.type !== 'verify');
  const hasPendingPrd = wi.some(w => w.type === 'plan-to-prd' && (w.status === 'pending' || w.status === 'dispatched'));
  const hasActiveWork = implementWi.some(w => w.status === 'pending' || w.status === 'dispatched');
  // A WI is "effectively done" when it succeeded normally OR when it is a
  // reconciled follow-up failure (intended outcome satisfied elsewhere — #2366).
  // The original failure record is preserved on the WI for audit; it just
  // stops blocking plan completion and stops driving the red badge.
  const isReconciledFailure = (w) =>
    w.status === 'failed' &&
    (w.itemType === 'pr' || w.itemType === 'verify') &&
    isFollowUpOutcomeSatisfied(w, workItems, allPrs);
  const isEffectivelyDone = (w) =>
    w.status === 'done' || w.status === 'decomposed' || isReconciledFailure(w);
  const allDone = implementWi.length > 0 && implementWi.every(isEffectivelyDone);
  const hasFailed = implementWi.some(w => w.status === 'failed' && !isReconciledFailure(w));

  // User-set statuses take priority when no work has started
  if (prdJsonStatus === 'rejected') return 'rejected';
  if (prdJsonStatus === 'paused' && !allDone) return 'paused';
  if (prdJsonStatus === 'revision-requested') return 'revision-requested';
  // PRD awaiting approval overrides WI-derived completion — new items may need materialization (#970)
  if (prdJsonStatus === 'awaiting-approval') return 'awaiting-approval';

  // Derive from work item progress
  if (allDone && !hasActiveWork) return 'completed';
  if (hasActiveWork) return 'dispatched';
  if (hasPendingPrd) return 'converting';
  if (hasFailed && !hasActiveWork) return 'has-failures';
  if (prdJsonStatus === 'approved' && implementWi.length === 0) return 'approved';

  // plan-to-prd conversion done but no implement items and PRD is gone — truly completed
  const prdConversionDone = wi.some(w => w.type === 'plan-to-prd' && w.status === 'done');
  if (prdConversionDone && implementWi.length === 0) return 'completed';

  return prdJsonStatus || 'active';
}

function renderPlans(plans) {
  plans = plans.filter(function(p) { return !isDeleted('plan:' + p.file); });
  const el = document.getElementById('plans-list');
  const countEl = document.getElementById('plans-count');
  countEl.textContent = plans.length;

  if (plans.length === 0) {
    el.innerHTML = '<p class="empty">No plans yet. Use /plan in the command center to create one.</p>';
    return;
  }

  const statusLabels = { 'awaiting-approval': 'Awaiting Approval', 'paused': 'Paused', 'approved': 'Approved', 'rejected': 'Rejected', 'revision-requested': 'Revision Requested', 'completed': 'Completed', 'active': 'Active' };
  const statusClass = (s) => s === 'awaiting-approval' || s === 'paused' ? 'awaiting' : s || '';
  const normalizeSourcePlanKey = (name) => {
    if (!name) return '';
    if (name.endsWith('.md') || name.endsWith('.json')) return name;
    return name + '.md';
  };

  // Check which plans have active dispatches or pending plan-to-prd work items
  const activeDisp = (window._lastDispatch?.active || []);
  const pendingDisp = (window._lastDispatch?.pending || []);
  const workingPlanFiles = new Set();
  for (const d of [...activeDisp, ...pendingDisp]) {
    const src = d.meta?.item?.sourcePlan || '';
    if (src) workingPlanFiles.add(src);
    // Match plan-to-prd tasks by planFile in item meta
    const planFile = d.meta?.item?.planFile || d.meta?.planFile || '';
    if (planFile) workingPlanFiles.add(planFile);
    // Also match by task description
    if (d.type === 'plan-to-prd' && d.task) {
      for (const p of plans) { if (d.task.includes(p.file?.replace('.md', '').replace('.json', '') || '___')) workingPlanFiles.add(p.file); }
    }
  }
  // Check work items for pending/dispatched plan-to-prd or implement tasks
  const allWi = window._lastWorkItems || [];
  for (const w of allWi) {
    if (w.type === 'plan-to-prd' && (w.status === 'pending' || w.status === 'dispatched') && w.planFile) {
      workingPlanFiles.add(w.planFile);
    }
    // Also track which PRD .json files have active work
    if (w.sourcePlan && (w.status === 'dispatched' || w.status === 'pending')) {
      workingPlanFiles.add(w.sourcePlan);
    }
  }

  // Link .md plans to their PRD .json — if a PRD is being worked on, the source plan is too.
  // The .md → .json mapping is materialized in planToPrdFile below; here we only need to
  // know whether any PRD is active to mark all candidate source .md plans as working.
  const workingJsons = new Set([...workingPlanFiles].filter(f => f.endsWith('.json')));
  if (workingJsons.size > 0) {
    for (const p of plans) {
      if (p.format === 'draft' && p.file.endsWith('.md')) workingPlanFiles.add(p.file);
    }
  }

  // Track which .md plans have a paused PRD, and map .md → PRD .json
  // (status derived from work items via derivePlanStatus — no separate tracking needed)
  const planToPrdFile = {}; // .md filename → .json PRD filename
  for (const p of plans) {
    if (p.format === 'prd' && !p.archived && p.sourcePlan) {
      const sourceKeys = [p.sourcePlan, normalizeSourcePlanKey(p.sourcePlan)];
      for (const sourceKey of sourceKeys) {
        if (!sourceKey) continue;
        planToPrdFile[sourceKey] = p.file;
        // Status derived via derivePlanStatus — planToPrdFile mapping is all we need
      }
    }
  }

  const activePlans = plans.filter(p => !p.archived && p.format !== 'prd');
  const archivedPlans = plans.filter(p => p.archived && p.format !== 'prd');
  countEl.textContent = activePlans.length + (archivedPlans.length ? ' + ' + archivedPlans.length + ' archived' : '');

  function renderPlanCard(p) {
    const prdFile = planToPrdFile[p.file] || (p.file.endsWith('.json') ? p.file : '');
    const isArchived = p.archived;

    // For .md plans with a linked PRD, use the PRD's status as the authoritative intent
    let prdJsonStatus = p.status || 'active';
    if (prdFile && p.format !== 'prd') {
      const linkedPrd = plans.find(pp => pp.file === prdFile && pp.format === 'prd');
      if (linkedPrd) {
        prdJsonStatus = linkedPrd.status || prdJsonStatus;
        // Propagate archiveReady from PRD to source .md plan card
        if (linkedPrd.archiveReady) p.archiveReady = true;
      }
      else if (!linkedPrd) {
        const archivedPrd = archivedPlans.find(pp => pp.file === prdFile && pp.format === 'prd');
        if (archivedPrd) prdJsonStatus = 'completed';
      }
    }

    // Single source of truth: derive status from work items
    const effectiveStatus = isArchived ? 'completed' : derivePlanStatus(prdFile, p.file, prdJsonStatus, allWi);

    const statusLabelsMap = {
      'completed': 'Completed', 'dispatched': 'In Progress', 'converting': 'Converting to PRD',
      'paused': 'Paused', 'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved',
      'rejected': 'Rejected', 'revision-requested': 'Revision Requested',
      'has-failures': 'Has Failures', 'active': 'Active'
    };
    const label = statusLabelsMap[effectiveStatus] || effectiveStatus;
    const needsAction = (effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused') && !isArchived;
    const isRevision = effectiveStatus === 'revision-requested';
    const isCompleted = effectiveStatus === 'completed';
    const isDraft = p.format === 'draft' && !isCompleted;
    // For .md drafts: show Execute only if no PRD exists yet (not already executed)

    const isStale = p.planStale && !isArchived;

    let actions = '';
    if (needsAction && !isStale) {
      const actionTarget = prdFile || p.file;
      // For awaiting-approval: show Execute (re-generate PRD from updated plan) + Approve (use current PRD as-is)
      if (effectiveStatus === 'awaiting-approval' && isDraft && prdFile) {
        actions = '<div class="plan-card-actions" onclick="event.stopPropagation()">' +
          '<button class="plan-btn approve" onclick="planApprove(\'' + escapeHtml(actionTarget) + '\')">Approve</button>' +
          '<button class="plan-btn approve" style="opacity:0.7" onclick="planReexecuteModal(\'' + escapeHtml(p.file) + '\',\'' + escapeHtml(p.project || '') + '\')">Re-execute</button>' +
          '<button class="plan-btn reject" onclick="planReject(\'' + escapeHtml(actionTarget) + '\')">Reject</button>' +
        '</div>';
      } else {
        const actionLabel = effectiveStatus === 'paused' ? 'Resume' : 'Approve';
        actions = '<div class="plan-card-actions" onclick="event.stopPropagation()">' +
          '<button class="plan-btn approve" onclick="planApprove(\'' + escapeHtml(actionTarget) + '\')">' + actionLabel + '</button>' +
          '<button class="plan-btn reject" onclick="planReject(\'' + escapeHtml(actionTarget) + '\')">Reject</button>' +
        '</div>';
      }
    } else if (isRevision) {
      actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escapeHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
    }

    const executeBtn = isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green);font-weight:600" ' +
      'onclick="event.stopPropagation();planExecute(\'' + escapeHtml(p.file) + '\',\'' + escapeHtml(p.project) + '\',this)">Execute</button>' : '';
    const showPause = effectiveStatus === 'dispatched' && prdFile && !isArchived;
    // Resume pill not needed — paused state is handled by the actions block above
    const showResume = false;
    const verifyWi = allWi.find(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
    const hasVerifyWi = !!verifyWi;
    const showVerify = effectiveStatus === 'completed' && prdFile && !isArchived && !hasVerifyWi;
    const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow)" ' +
      'onclick="event.stopPropagation();planPause(\'' + escapeHtml(prdFile) + '\',this)">Pause</button>' : '';
    const resumeBtn = showResume
      ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green)" ' +
        'onclick="event.stopPropagation();planApprove(\'' + escapeHtml(prdFile) + '\',this)">Resume</button>'
      : '';
    const verifyBtn = showVerify ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--green)" ' +
      'onclick="event.stopPropagation();triggerVerify(\'' + escapeHtml(prdFile) + '\',this)">Verify</button>' : '';
    const showArchive = !isArchived;
    const archiveFile = prdFile || p.file;
    const archiveReady = p.archiveReady && !isArchived;
    const archiveBtn = showArchive ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px' +
      (archiveReady ? ';color:var(--green);font-weight:600;border:1px solid var(--green)' : '') + '" ' +
      'onclick="event.stopPropagation();planArchive(\'' + escapeHtml(archiveFile) + '\',this)">' +
      (archiveReady ? '✓ Archive' : 'Archive') + '</button>' : '';
    const archiveReadyBadge = archiveReady ? '<span style="font-size:var(--text-xs);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);vertical-align:middle" title="Verification passed — ready to archive">Ready to archive</span>' : '';
    const deleteBtn = !isArchived ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--red)" ' +
      'onclick="event.stopPropagation();planDelete(\'' + escapeHtml(p.file) + '\')">Delete</button>' : '';

    const versionBadge = p.version ? ' <span style="font-size:var(--text-xs);font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
    const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
    const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
    return '<div class="plan-card ' + cardClass + '" data-file="plans/' + escapeHtml(p.file) + '" style="cursor:pointer' + (isArchived ? ';opacity:0.7' : '') + '" onclick="if(shouldIgnoreSelectionClick(event))return;planView(\'' + escapeHtml(p.file) + '\')">' +
      '<div class="plan-card-header">' +
        '<div><div class="plan-card-title">' + escapeHtml(p.summary || p.file) + versionBadge + '</div>' +
          '<div class="plan-card-meta">' +
            '<span style="font-weight:600;color:' + (statusColors[effectiveStatus] || 'var(--muted)') + '">' + label + '</span>' +
            (p.project ? '<span>' + escapeHtml(p.project) + '</span>' : '') +
            '<span>' + p.itemCount + ' items</span>' +
            (p.updatedAt ? '<span title="Last updated: ' + p.updatedAt + '">Updated ' + timeAgo(p.updatedAt) + '</span>' : '') +
            (p.completedAt ? '<span>' + p.completedAt.slice(0, 10) + '</span>' : '') +
            (p.generatedBy ? '<span>by ' + escapeHtml(p.generatedBy) + '</span>' : '') +
            executeBtn + pauseBtn + resumeBtn + verifyBtn + (hasVerifyWi ? _renderVerifyBadge(verifyWi) : '') + archiveReadyBadge + archiveBtn + deleteBtn +
          '</div>' +
        '</div>' +
      '</div>' +
      actions +
    '</div>';
  }

  const totalPlanPages = Math.ceil(activePlans.length / PLANS_PER_PAGE);
  if (_plansPage >= totalPlanPages) _plansPage = totalPlanPages - 1;
  if (_plansPage < 0) _plansPage = 0;
  const plansStart = _plansPage * PLANS_PER_PAGE;
  const pagePlans = activePlans.slice(plansStart, plansStart + PLANS_PER_PAGE);

  let html = pagePlans.map(renderPlanCard).join('');

  if (activePlans.length > PLANS_PER_PAGE) {
    html += '<div class="pr-pager">' +
      '<span class="pr-page-info">' + (plansStart + 1) + '-' + Math.min(plansStart + PLANS_PER_PAGE, activePlans.length) + ' of ' + activePlans.length + '</span>' +
      '<div class="pr-pager-btns">' +
        '<button class="pr-pager-btn ' + (_plansPage === 0 ? 'disabled' : '') + '" onclick="_plansPrev()">Prev</button>' +
        '<button class="pr-pager-btn ' + (_plansPage >= totalPlanPages - 1 ? 'disabled' : '') + '" onclick="_plansNext()">Next</button>' +
      '</div>' +
    '</div>';
  }

  if (archivedPlans.length > 0) {
    window._archivedPlans = archivedPlans;
    window._archivedPlanRenderer = renderPlanCard;
    html += '<div style="margin-top:8px;text-align:right;position:relative" data-file="plan-archives">' +
      '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:3px 10px;color:var(--muted)" onclick="openArchivedPlansModal()">' +
        'View Archives (' + archivedPlans.length + ')' +
      '</button>' +
    '</div>';
  }

  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: plan file, summary, project, generatedBy, revision feedback, action target files)
  el.innerHTML = html;
  restoreNotifBadges();
}

function openArchivedPlansModal() {
  const plans = (window._archivedPlans || []).slice().sort((a, b) =>
    (b.completedAt || b.updatedAt || '').localeCompare(a.completedAt || a.updatedAt || '')
  );
  const render = window._archivedPlanRenderer;
  if (!plans.length || !render) return;

  const html = plans.map(p => {
    const itemCount = p.itemCount || 0;
    const completed = p.completedAt ? p.completedAt.slice(0, 10) : '';
    return '<div class="plan-card" data-file="plans/' + escapeHtml(p.file) + '" style="cursor:pointer;opacity:0.8" onclick="if(shouldIgnoreSelectionClick(event))return;planView(\'' + escapeHtml(p.file) + '\')">' +
      '<div class="plan-card-header">' +
        '<div><div class="plan-card-title" style="font-size:var(--text-lg)">' + escapeHtml(p.summary || p.file) + '</div>' +
          '<div class="plan-card-meta">' +
            '<span style="color:var(--green);font-weight:600">Completed</span>' +
            (p.project ? '<span>' + escapeHtml(p.project) + '</span>' : '') +
            '<span>' + itemCount + ' items</span>' +
            (completed ? '<span>' + completed + '</span>' : '') +
            (p.generatedBy ? '<span>by ' + escapeHtml(p.generatedBy) + '</span>' : '') +
          '</div>' +
        '</div>' +
      '</div>' +
    '</div>';
  }).join('');

  document.getElementById('modal-title').textContent = 'Archived Plans';
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: archived plan file, summary, project, generatedBy)
  document.getElementById('modal-body').innerHTML = html;
  document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
  document.getElementById('modal-body').style.whiteSpace = 'normal';
  document.getElementById('modal').classList.add('open');
}

// Disable all PRD action buttons to prevent double-clicks

async function planExecute(file, project, btn) {
  if (btn) { btn.textContent = 'Executing...'; btn.disabled = true; btn.style.color = 'var(--blue)'; }
  try {
    const res = await fetch('/api/plans/execute', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file, project })
    });
    const data = await res.json();
    if (res.ok) {
      // Inject into local work items so the next render immediately hides Execute
      if (!data.alreadyQueued && data.id) {
        (window._lastWorkItems = window._lastWorkItems || []).push({
          id: data.id, type: 'plan-to-prd', status: 'pending', planFile: file
        });
      }
      closeModal();
      // Fall back to a clean message if the server didn't return an id (#2375).
      const idLabel = data.id ? ' (' + data.id + ')' : '';
      const queuedLabel = data.id ? 'Queued ' + data.id + ' — agent will convert plan to PRD' : 'Plan queued — agent will convert plan to PRD';
      showToast('cmd-toast', data.alreadyQueued ? 'Already queued' + idLabel : queuedLabel, true);
      wakeEngine();
      refreshPlans();
    } else {
      if (btn) { btn.textContent = 'Execute'; btn.disabled = false; btn.style.color = 'var(--green)'; }
      alert('Failed: ' + (data.error || 'unknown'));
    }
  } catch (e) {
    if (btn) { btn.textContent = 'Execute'; btn.disabled = false; btn.style.color = 'var(--green)'; }
    alert('Error: ' + e.message);
  }
}

function planReexecuteModal(file, project) {
  const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
  document.getElementById('modal-title').textContent = 'Re-execute Plan';
  document.getElementById('modal-body').innerHTML =
    '<div style="display:flex;flex-direction:column;gap:10px">' +
      '<div style="font-size:var(--text-md);color:var(--muted)">' + escapeHtml(file) + '</div>' +
      '<label style="color:var(--text);font-size:var(--text-md)">Steer the regeneration — what should be fixed in the PRD?' +
        '<textarea id="plan-reexec-feedback" rows="6" style="' + inputStyle + ';resize:vertical" placeholder="(Optional) Describe what was wrong with the previous PRD or what the agent should change..."></textarea>' +
      '</label>' +
      '<div style="font-size:var(--text-base);color:var(--muted)">Leave blank to re-run without steering. The agent will read the existing PRD and rewrite it.</div>' +
      '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
        '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
        '<button onclick="planReexecuteSubmit(\'' + escapeHtml(file) + '\',\'' + escapeHtml(project || '') + '\')" class="btn-primary-lg">Re-execute</button>' +
      '</div>' +
    '</div>';
  document.getElementById('modal').classList.add('open');
  setTimeout(() => document.getElementById('plan-reexec-feedback')?.focus(), 100);
}

async function planReexecuteSubmit(file, project) {
  const feedback = (document.getElementById('plan-reexec-feedback')?.value || '').trim();
  // Optimistic: inject a pending plan-to-prd WI so derivePlanStatus → 'converting' on next render
  (window._lastWorkItems = window._lastWorkItems || []).push({
    id: 'optimistic-' + Date.now(),
    type: 'plan-to-prd', status: 'pending', planFile: file,
  });
  try { closeModal(); } catch { /* expected */ }
  showToast('cmd-toast', 'Re-executing plan' + (feedback ? ' with steering' : '') + '...', true);
  if (typeof renderPlans === 'function' && window._lastPlans) renderPlans(window._lastPlans);
  try {
    const res = await fetch('/api/plans/execute', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file, project, feedback })
    });
    const data = await res.json();
    if (!res.ok) {
      showToast('cmd-toast', 'Failed: ' + (data.error || 'unknown'), false);
      if (typeof refresh === 'function') refresh();
      return;
    }
    if (data.alreadyQueued) {
      showToast('cmd-toast', 'Already queued — feedback will not apply this run', true);
    }
    if (typeof wakeEngine === 'function') wakeEngine();
    if (typeof refreshPlans === 'function') refreshPlans();
  } catch (e) {
    showToast('cmd-toast', 'Error: ' + e.message, false);
    if (typeof refresh === 'function') refresh();
  }
}

async function planSubmitRevise(file) {
  const id = 'revise-feedback-' + file.replace(/\./g, '-');
  const feedback = document.getElementById(id).value.trim();
  if (!feedback) { showToast('cmd-toast', 'Please enter feedback', false); return; }
  try {
    const res = await fetch('/api/plans/revise', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, feedback }) });
    const data = await res.json();
    if (!res.ok) { showToast('cmd-toast', 'Revision failed: ' + (data.error || 'unknown'), false); return; }
    showToast('cmd-toast', 'Revision requested — agent will update the plan (' + data.workItemId + ')', true);
    planHideRevise(file);
    refreshPlans();
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
}

function planShowRevise(file) {
  const id = 'revise-input-' + file.replace(/\./g, '-');
  document.getElementById(id).style.display = 'block';
}

function planHideRevise(file) {
  const id = 'revise-input-' + file.replace(/\./g, '-');
  document.getElementById(id).style.display = 'none';
}

let _planPollInterval = null;
let _planPollFile = null;
let _planPollLastRaw = null;

function _stopPlanPoll() {
  if (_planPollInterval) { clearInterval(_planPollInterval); _planPollInterval = null; }
  _planPollFile = null;
  _planPollLastRaw = null;
}

function _renderPlanModal(normalizedFile, raw, lastMod) {
  let title = normalizedFile;
  let text = '';

  if (normalizedFile.endsWith('.json')) {
    let plan;
    try { plan = JSON.parse(raw); } catch (e) {
      document.getElementById('modal-body').innerHTML = '<p style="color:var(--red)">Failed to parse plan JSON: ' + escapeHtml(e.message) + '</p><pre style="font-size:var(--text-sm);max-height:200px;overflow:auto">' + escapeHtml((raw || '').slice(0, 500)) + '</pre>';
      return;
    }
    title = plan.plan_summary || normalizedFile;
    const items = (plan.missing_features || []).map((f, i) =>
      (i + 1) + '. [' + f.id + '] ' + f.name + ' (' + (f.estimated_complexity || '?') + ', ' + (f.priority || '?') + ')' +
      (f.depends_on?.length ? ' \u2192 depends on: ' + f.depends_on.join(', ') : '') +
      '\n   ' + (f.description || '') +
      (f.acceptance_criteria?.length ? '\n   Criteria: ' + f.acceptance_criteria.join('; ') : '')
    ).join('\n\n');
    text = 'Project: ' + (plan.project || '?') +
      '\nStrategy: ' + (plan.branch_strategy || 'parallel') +
      '\nBranch: ' + (plan.feature_branch || 'per-item') +
      '\nStatus: ' + (plan.status || 'active') +
      '\nGenerated by: ' + (plan.generated_by || '?') + ' on ' + (plan.generated_at || '?') +
      '\n\n--- Items (' + (plan.missing_features || []).length + ') ---\n\n' + items +
      (plan.open_questions?.length ? '\n\n--- Open Questions ---\n\n' + plan.open_questions.map(q => '\u2022 ' + q).join('\n') : '');
  } else {
    text = raw;
    const titleMatch = raw.match(/^#\s+(?:Plan:\s*)?(.+)/m);
    if (titleMatch) title = titleMatch[1];
  }

  const vMatch = normalizedFile.match(/-v(\d+)/);
  const versionLabel = vMatch ? ' (v' + vMatch[1] + ')' : '';
  const isMdPlan = normalizedFile.endsWith('.md');
  let planStatus = '';
  try { if (normalizedFile.endsWith('.json')) planStatus = JSON.parse(raw).status || ''; } catch {}
  // Modal buttons mirror card logic — use cached plans data (from /api/plans, not /api/status)
  const allPlans = window._lastPlans || [];
  const cardPlan = allPlans.find(p => p.file === normalizedFile || p.sourcePlan === normalizedFile || (p.file?.endsWith('.json') && p.file === normalizedFile));
  // Also check for linked PRD by searching all plans for a PRD whose source_plan matches this file
  const linkedPrd = allPlans.find(p => p.sourcePlan === normalizedFile && p.format === 'prd');
  const prdFile = cardPlan?.file?.endsWith('.json') ? cardPlan.file : (linkedPrd?.file || '');
  const prdJsonStatus = linkedPrd?.status || (cardPlan?.file?.endsWith('.json') ? cardPlan.status : '') || planStatus || '';
  const allWi = window._lastWorkItems || [];
  const effectiveStatus = (prdFile || normalizedFile) ? derivePlanStatus(prdFile, normalizedFile, prdJsonStatus, allWi) : (planStatus || 'active');
  const isArchived = !!(cardPlan?.archived || linkedPrd?.archived);
  const isCompleted = effectiveStatus === 'completed';
  const isDraft = isMdPlan && !prdFile && !isCompleted;
  const isNeedsAction = (effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused') && !isArchived;

  const bs = 'font-size:var(--text-sm);padding:2px 10px'; // button style
  let modalActions = '';

  // Approve/Reject for awaiting-approval or paused
  if (isNeedsAction) {
    const target = prdFile || normalizedFile;
    const label = effectiveStatus === 'paused' ? 'Resume' : 'Approve';
    modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="planApprove(\'' + escapeHtml(target) + '\',this)">' + label + '</button> ';
    // Re-execute: re-generate PRD from updated plan (only for .md plans with existing awaiting PRD)
    if (effectiveStatus === 'awaiting-approval' && isMdPlan && prdFile) {
      modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green);opacity:0.7" onclick="planReexecuteModal(\'' + escapeHtml(normalizedFile) + '\',\'\')">Re-execute</button> ';
    }
    modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planReject(\'' + escapeHtml(target) + '\')">Reject</button> ';
  }
  // Execute (draft .md without PRD)
  if (isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived) {
    modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green);font-weight:600" onclick="planExecute(\'' + escapeHtml(normalizedFile) + '\',\'\',this)">Execute</button> ';
  }
  // Pause (active PRD, not completed)
  if (effectiveStatus === 'dispatched' && prdFile && !isArchived) {
    modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--yellow)" onclick="planPause(\'' + escapeHtml(prdFile) + '\',this)">Pause</button> ';
  }
  // Completed label
  if (isCompleted && !isArchived) {
    modalActions += '<span style="' + bs + ';color:var(--green);font-weight:600">Completed</span> ';
  }
  // In progress label
  if (effectiveStatus === 'dispatched') {
    modalActions += '<span style="' + bs + ';color:var(--blue)">In Progress</span> ';
  }
  // Verify / Verified badge
  const modalVerifyWi = (window._lastWorkItems || []).find(w => w.itemType === 'verify' && w.sourcePlan === (prdFile || normalizedFile));
  if (effectiveStatus === 'completed' && prdFile && !isArchived && !modalVerifyWi) {
    modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="triggerVerify(\'' + escapeHtml(prdFile) + '\',this)">Verify</button> ';
  }
  if (modalVerifyWi) modalActions += _renderVerifyBadge(modalVerifyWi);
  // Archive + Delete (always, unless archived)
  if (!isArchived) {
    modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--muted)" onclick="planArchive(\'' + escapeHtml(prdFile || normalizedFile) + '\')">Archive</button> ';
    modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planDelete(\'' + escapeHtml(normalizedFile) + '\')">Delete</button>';
  }

  const lastModLabel = lastMod ? '<div style="font-size:var(--text-sm);color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + formatLocalDateTime(lastMod) + '</div>' : '';
  const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' + modalActions + '</div>';

  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: plan title, version label, action target files)
  document.getElementById('modal-title').innerHTML = escapeHtml(title) + (versionLabel ? ' <span style="font-size:var(--text-base);font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escapeHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
  const modalBody = document.getElementById('modal-body');
  const scrollTop = modalBody.scrollTop;
  if (normalizedFile.endsWith('.json')) {
    modalBody.textContent = text;
    modalBody.style.fontFamily = 'Consolas, monospace';
    modalBody.style.whiteSpace = 'pre-wrap';
  } else {
    // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
    modalBody.innerHTML = renderMd(text);
  }
  modalBody.scrollTop = scrollTop;

  return { title, text };
}

async function planView(file) {
  _stopPlanPoll();
  try {
    const normalizedFile = normalizePlanFile(file);

    // Show modal immediately with loading state
    document.getElementById('modal-title').textContent = normalizedFile;
    document.getElementById('modal-body').innerHTML = '<p style="color:var(--muted)">Loading...</p>';
    document.getElementById('modal').classList.add('open');

    const planRes = await fetch('/api/plans/' + encodeURIComponent(normalizedFile));
    const lastMod = planRes.headers.get('Last-Modified');
    const resolvedPath = planRes.headers.get('X-Resolved-Path');
    const raw = await planRes.text();

    const { title, text } = _renderPlanModal(normalizedFile, raw, lastMod);
    _planPollLastRaw = raw;

    _modalDocContext = { title, content: text, selection: '' };
    _modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
    const card = findCardForFile(_modalFilePath);
    if (card) clearNotifBadge(card);

    // Live-poll while modal is open
    _planPollFile = normalizedFile;
    _planPollInterval = setInterval(function() {
      if (!document.getElementById('modal')?.classList?.contains('open') || _planPollFile !== normalizedFile) {
        _stopPlanPoll(); return;
      }
      fetch('/api/plans/' + encodeURIComponent(normalizedFile))
        .then(function(r) { return r.text().then(function(raw) { return { raw: raw, lastMod: r.headers.get('Last-Modified') }; }); })
        .then(function(d) { if (_planPollFile === normalizedFile && d.raw !== _planPollLastRaw) { _planPollLastRaw = d.raw; _renderPlanModal(normalizedFile, d.raw, d.lastMod); } })
        .catch(function() {});
    }, 3000);
  } catch (e) { console.error(e); }
}

async function planApprove(file, btn) {
  if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Approving...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
  showToast('cmd-toast', 'Plan approved — work will begin on next engine tick', true);

  // Optimistic UI: mutate cached state and re-render so both the plan card and
  // PRD view reflect 'approved' immediately. The .md card's display status
  // follows the linked PRD's status via lookup in renderPlanCard, so updating
  // the PRD entry covers both. Refresh after fetch reconciles with truth.
  const isPrd = typeof file === 'string' && file.endsWith('.json');
  if (Array.isArray(window._lastPlans)) {
    for (const p of window._lastPlans) {
      if (p.file === file) {
        p.status = 'approved';
        p.planStale = false;
        delete p.pausedAt;
      }
    }
    try { renderPlans(window._lastPlans); } catch { /* render is best-effort */ }
  }
  // Issue #2949 — prdProgress moved off /api/status to /api/prd; refresh.js
  // stashes the fresh fetch into window._lastPrdProgress.
  const _prdProgressForApprove = window._lastPrdProgress;
  if (isPrd && _prdProgressForApprove?.items) {
    for (const item of _prdProgressForApprove.items) {
      if (item.source === file) {
        item.planStatus = 'approved';
        item.planStale = false;
      }
    }
    if (typeof rerenderPrdFromCache === 'function') rerenderPrdFromCache();
  }

  try {
    const res = await fetch('/api/plans/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
    if (res.ok) {
      refreshPlans();
      refresh();
    } else {
      if (btn) { btn.textContent = btn.dataset.origText || 'Approve'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
      const d = await res.json().catch(() => ({}));
      showToast('cmd-toast', 'Approve failed: ' + (d.error || 'unknown'), false);
      // Refresh to revert optimistic state from server truth
      refresh();
    }
  } catch (e) {
    if (btn) { btn.textContent = btn.dataset.origText || 'Approve'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
    showToast('cmd-toast', 'Error: ' + e.message, false);
    refresh();
  }
}

async function planDelete(file) {
  _stopPlanPoll();
  var isPrd = file.endsWith('.json');
  var confirmMsg = isPrd
    ? 'Delete PRD "' + file + '"? This cannot be undone. The source plan will be kept.'
    : 'Delete plan "' + file + '"? This cannot be undone.';
  if (!confirm(confirmMsg)) return;
  showToast('cmd-toast', isPrd ? 'PRD deleted' : 'Plan deleted', true);
  markDeleted('plan:' + file);
  try { closeModal(); } catch { /* may not be open */ }
  if (window._lastPlans) renderPlans(window._lastPlans);
  if (typeof rerenderPrdFromCache === 'function') rerenderPrdFromCache();
  try {
    const res = await fetch('/api/plans/delete', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file })
    });
    if (res.ok) {
      refresh();
    } else {
      const d = await res.json().catch(() => ({}));
      clearDeleted('plan:' + file);
      showToast('cmd-toast', 'Delete failed: ' + (d.error || 'unknown'), false);
      refresh(); // revert optimistic
    }
  } catch (e) {
    clearDeleted('plan:' + file);
    showToast('cmd-toast', 'Error: ' + e.message, false);
    refresh();
  }
}

async function planArchive(file, btn) {
  var isPrd = file.endsWith('.json');
  var confirmMsg = isPrd
    ? 'Archive this PRD? The linked source plan will also be archived.'
    : 'Archive this plan?';
  if (!confirm(confirmMsg)) return;
  _stopPlanPoll();
  showToast('cmd-toast', isPrd ? 'Archiving PRD and linked source plan...' : 'Archiving plan...', true);
  markDeleted('plan:' + file);
  if (isPrd) {
    var prdRecord = (window._lastPlans || []).find(function(p) { return p.file === file && p.sourcePlan; });
    if (prdRecord) markDeleted('plan:' + prdRecord.sourcePlan);
  }
  if (window._lastPlans) renderPlans(window._lastPlans);
  if (typeof rerenderPrdFromCache === 'function') rerenderPrdFromCache();
  try { closeModal(); } catch { /* may not be open */ }
  try {
    const res = await fetch('/api/plans/archive', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file })
    });
    const ct = res.headers.get('content-type') || '';
    if (!ct.includes('json')) { refresh(); return; }
    const d = await res.json().catch(() => ({}));
    if (res.ok && d.ok) {
      if (d.archivedSource || d.cancelledItems) {
        var msg = 'Archived';
        if (d.archivedSource) msg += ' PRD + source plan';
        if (d.cancelledItems) msg += ', cancelled ' + d.cancelledItems + ' pending item(s)';
        showToast('cmd-toast', msg, true);
      }
      refresh();
    } else {
      clearDeleted('plan:' + file);
      if (prdRecord) clearDeleted('plan:' + prdRecord.sourcePlan);
      showToast('cmd-toast', 'Archive failed: ' + (d.error || 'unknown'), false);
      refresh();
    }
  } catch (e) { clearDeleted('plan:' + file); if (prdRecord) clearDeleted('plan:' + prdRecord.sourcePlan); showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
}

async function planPause(file, btn) {
  if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Pausing...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
  showToast('cmd-toast', 'Plan paused — no new items will be dispatched', true);
  try {
    const res = await fetch('/api/plans/pause', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
    if (res.ok) {
      refreshPlans();
      refresh();
    } else {
      if (btn) { btn.textContent = btn.dataset.origText || 'Pause'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
      const d = await res.json().catch(() => ({}));
      showToast('cmd-toast', 'Pause failed: ' + (d.error || 'unknown'), false);
    }
  } catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Pause'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } showToast('cmd-toast', 'Error: ' + e.message, false); }
}

async function planReject(file) {
  if (!confirm('Reject this plan? It will not be executed.')) return;
  const reason = prompt('Reason for rejection (optional):') || '';
  showToast('cmd-toast', 'Plan rejected', true);
  try {
    const res = await fetch('/api/plans/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, reason }) });
    if (res.ok) {
      refreshPlans();
    } else {
      const d = await res.json().catch(() => ({}));
      showToast('cmd-toast', 'Reject failed: ' + (d.error || 'unknown'), false);
    }
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
}

async function planDiscuss(file) {
  try {
    const res = await fetch('/api/plans/discuss', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file })
    });
    const data = await res.json();
    if (!res.ok) throw new Error(data.error);

    // Show the launch command in a modal
    const content = `To discuss and revise this plan interactively, run this command in a terminal:\n\n` +
      `━━━ Bash / Git Bash ━━━\n${data.command}\n\n` +
      `━━━ PowerShell ━━━\n${data.psCommand}\n\n` +
      `━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
      `This launches an interactive Claude session with the plan pre-loaded.\n` +
      `Chat naturally to review and refine. When you're satisfied, say "approve" and the session will write the approved plan back to disk.\n\n` +
      `The engine will pick it up on the next tick and start dispatching work.`;

    document.getElementById('modal-title').textContent = 'Discuss Plan: ' + file;
    document.getElementById('modal-body').textContent = content;
    document.getElementById('modal').classList.add('open');
  } catch (e) {
    showToast('cmd-toast', 'Error: ' + e.message, false);
  }
}

async function planOpenInDocChat(file) {
  try {
    document.getElementById('modal-title').textContent = file;
    document.getElementById('modal-body').innerHTML = '<p style="color:var(--muted)">Loading...</p>';
    document.getElementById('modal').classList.add('open');
    const normalizedFile = normalizePlanFile(file);
    const planRes = await fetch('/api/plans/' + encodeURIComponent(normalizedFile));
    const resolvedPath = planRes.headers.get('X-Resolved-Path');
    const raw = await planRes.text();
    let title = normalizedFile;
    let text = raw;
    if (normalizedFile.endsWith('.json')) {
      try { title = JSON.parse(raw).plan_summary || file; } catch {}
    }
    document.getElementById('modal-title').textContent = 'Edit: ' + title;
    if (normalizedFile.endsWith('.json')) {
      document.getElementById('modal-body').textContent = text;
      document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
      document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
    } else {
      // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
      document.getElementById('modal-body').innerHTML = renderMd(text);
    }
    _modalDocContext = { title: title, content: text, selection: '' };
    _modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
    const card = findCardForFile(_modalFilePath);
    if (card) clearNotifBadge(card);
    document.getElementById('modal').classList.add('open');
  } catch (e) { alert('Error opening plan: ' + e.message); }
}

async function planRegeneratePRD(source) {
  if (!confirm('Reset pending/failed items to pick up plan changes?\n\nIn-progress and completed items won\'t be affected.')) return;
  try {
    const res = await fetch('/api/plans/regenerate', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ source })
    });
    const d = await res.json();
    if (res.ok && d.ok) {
      refresh();
      showToast('cmd-toast', 'Regenerated: ' + d.reset + ' reset, ' + d.kept + ' kept, ' + d.new + ' new', true);
    } else {
      alert('Failed: ' + (d.error || 'unknown'));
    }
  } catch (e) { alert('Error: ' + e.message); }
}

function _renderVerifyBadge(verifyWi) {
  const statusColors = { pending: 'var(--muted)', dispatched: 'var(--blue)', done: 'var(--green)', failed: 'var(--red)' };
  const color = statusColors[verifyWi.status] || 'var(--muted)';
  const label = verifyWi.status === 'dispatched' ? 'Verifying...' : verifyWi.status === 'done' ? '\u2714 Verified' : verifyWi.status === 'failed' ? 'Verify failed' : 'Verify pending';
  // E2E PR — check by prdItems, branch, or title. Issue #2949 — pullRequests
  // moved off /api/status to /api/pull-requests (window._lastPullRequests).
  const allPrs = window._lastPullRequests || [];
  const planFile = verifyWi.sourcePlan || '';
  const planSlug = planFile.replace('.json', '');
  const verifyPr = allPrs.find(pr => (pr.prdItems || []).includes(verifyWi.id) || (pr.branch && pr.branch.includes(planSlug) && (pr.title || '').includes('[E2E]')));
  const prLink = verifyPr?.url ? ' <a href="' + escapeHtml(verifyPr.url) + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--blue);text-decoration:underline;font-size:var(--text-xs)">' + escapeHtml(verifyPr.id || 'E2E PR') + '</a>' : '';
  // Testing guide. verifyGuides moved to /api/verify-guides
  // (window._lastVerifyGuides set by refresh.js).
  const guides = window._lastVerifyGuides || [];
  const guide = guides.find(g => g.planFile === planFile);
  const guideLink = guide ? ' <span onclick="event.stopPropagation();openVerifyGuide(\'' + escapeHtml(guide.file) + '\')" style="color:var(--green);cursor:pointer;text-decoration:underline;font-size:var(--text-xs)">Testing Guide</span>' : '';
  return '<span style="font-size:var(--text-xs);font-weight:600;color:' + color + ';padding:0 4px">' + label + '</span>' + prLink + guideLink;
}

async function openVerifyGuide(file) {
  try {
    const normalizedFile = normalizePlanFile(file);
    const content = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(r => r.text());
    document.getElementById('modal-title').innerHTML = 'Manual Testing Guide' +
      ' <button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;margin-left:8px;vertical-align:middle" onclick="openArchivedPrdModal()">Back</button>';
    // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js)
    document.getElementById('modal-body').innerHTML = renderMd(content);
    _modalDocContext = { title: 'Manual Testing Guide', content, selection: '' };
    _modalFilePath = 'prd/' + normalizedFile; showModalQa();
    const card = findCardForFile(_modalFilePath);
    if (card) clearNotifBadge(card);
    document.getElementById('modal').classList.add('open');
  } catch (e) { alert('Failed to load guide: ' + e.message); }
}

async function triggerVerify(file, btn) {
  if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Verifying...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
  showToast('cmd-toast', 'Verify task queued', true);
  try {
    const res = await fetch('/api/plans/trigger-verify', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file })
    });
    const d = await res.json();
    if (res.ok && d.ok) {
      try { closeModal(); } catch { /* may not be open */ }
      refresh();
    } else {
      if (btn) { btn.textContent = btn.dataset.origText || 'Verify'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
      alert('Failed: ' + (d.error || 'unknown'));
    }
  } catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Verify'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Error: ' + e.message); }
}

async function planUnarchive(file, btn) {
  try { closeModal(); } catch {}
  showToast('cmd-toast', 'Restored from archive', true);
  try {
    const res = await fetch('/api/plans/unarchive', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file })
    });
    if (res.ok) { refreshPlans(); refresh(); }
    else {
      const d = await res.json().catch(() => ({}));
      showToast('cmd-toast', 'Unarchive failed: ' + (d.error || 'unknown'), false);
      refresh();
    }
  } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
}

window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, isFollowUpOutcomeSatisfied, renderPlans, openArchivedPlansModal, planExecute, planReexecuteModal, planReexecuteSubmit, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
