Fix new-capture auto-hide, library capture bar, step delete, and click-capture timing
Template tests / tests (push) Successful in 1m49s

- New Capture sessions now start paused; the window only tucks away once
  the user clicks "Start recording" in the capture bar instead of hiding
  ~1.2s after starting.
- The capture status bar is shown only in the editor view, not over the
  library.
- Fix openModal/confirmDialog resolving as cancelled when an action button
  is clicked, which made the step "Delete" button (and other modal actions)
  silently no-op.
- Click-triggered captures now use the click-time cursor position for the
  marker and arm the capture cache as soon as recording starts, so the
  first click is captured instantly and accurately placed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Iisyourdad
2026-06-11 09:21:19 -05:00
parent 5aefbdacaa
commit 99376fdeb2
7 changed files with 213 additions and 50 deletions
+20 -9
View File
@@ -137,6 +137,9 @@ class StepForgeApp {
this.editorHost.classList.toggle('hidden', view !== 'editor');
this.searchInput.classList.toggle('hidden', view !== 'library');
this.renderTopbar();
// The capture bar is editor-only; re-evaluate its visibility now that
// the view changed.
this.updateCaptureState(this.captureState);
}
showWelcome() {
@@ -188,13 +191,15 @@ class StepForgeApp {
const state = await api.capture.session({ action: 'start', guideId: guide.guideId });
this.updateCaptureState(state);
const hotkey = this.state.settings?.capture?.hotkeyCapture;
let how;
if (state.clickCapture) {
toast('Recording — every click grabs a step. StepForge tucks away; use the red tray icon to pause or finish.');
how = 'every click will grab a step';
} else if (state.intervalSec > 0) {
toast(`Recording — a step every ${state.intervalSec}s. StepForge tucks away; use the red tray icon to pause or finish.`);
how = `a step will be grabbed every ${state.intervalSec}s`;
} else {
toast(hotkey ? `Recording — press ${hotkey} to grab steps. Use the red tray icon to pause or finish.` : 'Capture session started.');
how = hotkey ? `press ${hotkey} to grab steps` : 'use Shoot to grab steps';
}
toast(`Click "Start recording" in the red bar when you're ready — ${how}. StepForge tucks away; use the red tray icon to pause or finish.`);
}
async openExistingWorkspace() {
@@ -231,7 +236,10 @@ class StepForgeApp {
updateCaptureState(state) {
this.captureState = state || { active: false };
clearNode(this.captureStatus);
if (!this.captureState.active) {
// The capture bar only makes sense alongside the editor it's recording
// into — hide it everywhere else (e.g. the library) even if a session
// is still active in the background.
if (!this.captureState.active || this.state.view !== 'editor') {
this.captureStatus.classList.add('hidden');
return;
}
@@ -240,10 +248,12 @@ class StepForgeApp {
const send = (payload) => api.capture.session(payload).then((next) => this.updateCaptureState(next));
// What is currently triggering captures, so the user knows what to do.
const trigger = s.paused ? 'paused'
: s.clickCapture ? 'on click'
: s.intervalSec > 0 ? `every ${s.intervalSec}s`
: 'hotkey only';
const notStarted = s.paused && !s.count;
const trigger = notStarted ? 'ready'
: s.paused ? 'paused'
: s.clickCapture ? 'on click'
: s.intervalSec > 0 ? `every ${s.intervalSec}s`
: 'hotkey only';
const shootBtn = el('button', {
type: 'button',
@@ -261,8 +271,9 @@ class StepForgeApp {
const pauseBtn = el('button', {
type: 'button',
title: notStarted ? 'StepForge tucks away and starts capturing' : '',
onClick: () => send({ action: s.paused ? 'resume' : 'pause' }),
}, s.paused ? 'Resume' : 'Pause');
}, notStarted ? 'Start recording' : s.paused ? 'Resume' : 'Pause');
const finishBtn = el('button', {
type: 'button',
+1 -1
View File
@@ -1146,7 +1146,7 @@ class GuideEditor {
async startCaptureSession() {
await api.capture.session({ action: 'start', guideId: this.guideId });
this.onToast('Capture session started.');
this.onToast('Capture session ready — click "Start recording" in the red bar when you\'re set.');
this.emitMeta();
}
+10 -3
View File
@@ -56,23 +56,30 @@ function toast(message, { error = false, ms = 2600 } = {}) {
function openModal({ title, body, footer, wide = false, onClose }) {
const root = document.getElementById('modal-root');
clearNode(root);
// `close` just tears down the modal. Buttons that already resolve the
// dialog's promise themselves call this. `dismiss` additionally fires
// `onClose`, for ways of leaving the dialog that didn't pick an option
// (Esc, the ✕, or clicking the backdrop) and need a default resolution.
const close = () => {
clearNode(root);
document.removeEventListener('keydown', escHandler, true);
};
const dismiss = () => {
close();
if (onClose) onClose();
};
const escHandler = (e) => {
if (e.key === 'Escape') { e.stopPropagation(); close(); }
if (e.key === 'Escape') { e.stopPropagation(); dismiss(); }
};
document.addEventListener('keydown', escHandler, true);
const modal = el('div.modal', { className: `modal${wide ? ' wide' : ''}` },
el('header', {}, title, el('span.close', { onClick: close, title: 'Close (Esc)' }, '✕')),
el('header', {}, title, el('span.close', { onClick: dismiss, title: 'Close (Esc)' }, '✕')),
el('div.body', {}, body),
footer ? el('footer', {}, footer) : null,
);
modal.addEventListener('click', (e) => e.stopPropagation());
root.append(modal);
root.onclick = close;
root.onclick = dismiss;
return { close, node: modal };
}