From e8f6e4cd094f612861ca03666e56e7110df381e0 Mon Sep 17 00:00:00 2001 From: Iisyourdad Date: Fri, 12 Jun 2026 13:53:16 -0500 Subject: [PATCH 01/11] Require recording acknowledgment before hide --- app/capture.js | 37 ++++++++++++++++++++++++++- app/main.js | 1 + tests/unit/capture.test.js | 51 +++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/app/capture.js b/app/capture.js index fb5e293..acf138d 100644 --- a/app/capture.js +++ b/app/capture.js @@ -103,11 +103,19 @@ function hasBinary(name) { } class CaptureService { - constructor({ store, settings, getWindow, notify, screenApi = screen }) { + constructor({ + store, + settings, + getWindow, + notify, + screenApi = screen, + dialogApi = null, + }) { this.store = store; this.settings = settings; this.getWindow = getWindow; this.notify = notify; + this.dialog = dialogApi; // Injectable for tests; the click/coordinate paths must never reach for // the global `screen` directly so coordinate handling stays testable. this.screen = screenApi; @@ -134,6 +142,7 @@ class CaptureService { // True only while a resume is warming up (window still visible, buffer // not yet primed). Clicks are ignored until it clears — see armRecording. this.warmingUp = false; + this.sessionInstructionsShown = false; } state() { @@ -185,6 +194,7 @@ class CaptureService { // New Capture never makes the window vanish out from under them. this.session = { guideId, paused: true, count: 0, intervalSec: interval }; this.sessionNotificationShown = false; + this.sessionInstructionsShown = false; if (this.settings.get('capture.captureOutsideClicks') !== false) this.startClickWatcher(); this.applyInterval(); this.notify('capture:state', this.state()); @@ -375,6 +385,11 @@ class CaptureService { if (!this.session || this.session.paused) { this.warmingUp = false; return; } } if (wantHide && win && !win.isDestroyed() && win.isVisible()) { + if (!this.sessionInstructionsShown) { + this.sessionInstructionsShown = true; + await this.showRecordingInstructions(win); + if (!this.session || this.session.paused) { this.warmingUp = false; return; } + } win.hide(); // Let a couple of frames of the now-unobscured screen land before // the user's first click, so that frame shows their work, not the @@ -396,6 +411,25 @@ class CaptureService { run().catch(() => { this.warmingUp = false; }); } + async showRecordingInstructions(win) { + if (!this.dialog || typeof this.dialog.showMessageBox !== 'function') return; + try { + await this.dialog.showMessageBox(win, { + type: 'info', + title: 'StepForge recording', + message: 'Please go into the tray icon and select the red button to stop recording.', + detail: 'Click OK to continue and hide this window.', + buttons: ['OK'], + defaultId: 0, + cancelId: 0, + noLink: true, + }); + } catch { + // If the dialog cannot be shown, keep the session moving instead of + // leaving the user stuck on the start flow. + } + } + finishSession() { if (this.intervalTimer) { clearInterval(this.intervalTimer); @@ -408,6 +442,7 @@ class CaptureService { this.destroySessionTray(); this.session = null; this.sessionNotificationShown = false; + this.sessionInstructionsShown = false; if (this.hiddenForSession) { this.hiddenForSession = false; this.showWindow(); diff --git a/app/main.js b/app/main.js index f929f09..f8ff1c5 100644 --- a/app/main.js +++ b/app/main.js @@ -672,6 +672,7 @@ if (!gotLock) { settings, getWindow: () => mainWindow, notify: sendToRenderer, + dialogApi: dialog, }); applyTheme(); diff --git a/tests/unit/capture.test.js b/tests/unit/capture.test.js index 8ec842b..52c5b4d 100644 --- a/tests/unit/capture.test.js +++ b/tests/unit/capture.test.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict'); const CaptureService = require('../../app/capture'); -function makeService({ settings: settingsOverrides, screenApi } = {}) { +function makeService({ settings: settingsOverrides, screenApi, dialogApi } = {}) { const store = { addStep() { throw new Error('not used in this test'); @@ -26,6 +26,9 @@ function makeService({ settings: settingsOverrides, screenApi } = {}) { settings, getWindow: () => null, notify: () => {}, + dialogApi: dialogApi || { + showMessageBox: async () => ({ response: 0 }), + }, screenApi: screenApi || { getCursorScreenPoint: () => ({ x: 0, y: 0 }), getAllDisplays: () => [], @@ -626,6 +629,52 @@ test('armRecording warms while visible, then hides and arms the session', async service.finishSession(); }); +test('armRecording shows a blocking instruction dialog before the window hides', async () => { + const service = makeService(); + const win = { + destroyed: false, visible: true, + isDestroyed() { return this.destroyed; }, + isVisible() { return this.visible; }, + isMinimized() { return false; }, + hide() { this.visible = false; }, + show() { this.visible = true; }, + focus() {}, getTitle() { return 'StepForge'; }, + getBounds() { return { x: 0, y: 0, width: 800, height: 600 }; }, + }; + service.getWindow = () => win; + service.clickCaptureAvailable = () => false; + service.hiddenForSession = true; + service.session = { guideId: 'g-prompt', paused: true, count: 0, intervalSec: 0 }; + + let releaseDialog; + const dialogGate = new Promise((resolve) => { releaseDialog = resolve; }); + let seenOptions = null; + service.dialog = { + showMessageBox: async (_win, options) => { + seenOptions = options; + await dialogGate; + return { response: 0 }; + }, + }; + + service.togglePause(false); + + for (let i = 0; i < 40 && !seenOptions; i++) { + await new Promise((r) => setTimeout(r, 25)); + } + assert.ok(seenOptions, 'the instruction dialog must appear before the window hides'); + assert.equal(seenOptions?.message, 'Please go into the tray icon and select the red button to stop recording.'); + assert.equal(win.visible, true, 'the window must stay visible until the dialog is acknowledged'); + + releaseDialog(); + for (let i = 0; i < 20 && win.visible; i++) { + await new Promise((r) => setTimeout(r, 25)); + } + + assert.equal(win.visible, false, 'the window hides only after the acknowledgement dialog is dismissed'); + service.finishSession(); +}); + test('a slow recorder start still arms within the warmup cap', async () => { // If the backend start hangs (Windows can take seconds), the window must // still hide and recording must still arm — the restart bug was the From 85b1f6f1433fbac2150ac6262b7c757cdf00b3d2 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 13:50:13 -0500 Subject: [PATCH 02/11] Use in-app modal for recording start confirmation --- app/capture.js | 52 +--------------------------------- app/main.js | 1 - app/renderer/app.js | 8 +++++- app/renderer/dialogs.js | 22 +++++++++++++++ app/renderer/style.css | 24 ++++++++++++++++ tests/unit/capture.test.js | 57 +++----------------------------------- 6 files changed, 58 insertions(+), 106 deletions(-) diff --git a/app/capture.js b/app/capture.js index acf138d..b380094 100644 --- a/app/capture.js +++ b/app/capture.js @@ -2,7 +2,7 @@ const path = require('node:path'); const { spawn, execFileSync } = require('node:child_process'); -const { desktopCapturer, screen, BrowserWindow, nativeImage, Tray, Menu, Notification } = require('electron'); +const { desktopCapturer, screen, BrowserWindow, nativeImage, Tray, Menu } = require('electron'); const { expandPlaceholders } = require('../core/placeholders'); const raster = require('../core/raster'); const { encodePng } = require('../core/png'); @@ -109,13 +109,11 @@ class CaptureService { getWindow, notify, screenApi = screen, - dialogApi = null, }) { this.store = store; this.settings = settings; this.getWindow = getWindow; this.notify = notify; - this.dialog = dialogApi; // Injectable for tests; the click/coordinate paths must never reach for // the global `screen` directly so coordinate handling stays testable. this.screen = screenApi; @@ -142,7 +140,6 @@ class CaptureService { // True only while a resume is warming up (window still visible, buffer // not yet primed). Clicks are ignored until it clears — see armRecording. this.warmingUp = false; - this.sessionInstructionsShown = false; } state() { @@ -193,8 +190,6 @@ class CaptureService { // the user explicitly presses "Start recording" in the capture bar, so // New Capture never makes the window vanish out from under them. this.session = { guideId, paused: true, count: 0, intervalSec: interval }; - this.sessionNotificationShown = false; - this.sessionInstructionsShown = false; if (this.settings.get('capture.captureOutsideClicks') !== false) this.startClickWatcher(); this.applyInterval(); this.notify('capture:state', this.state()); @@ -362,7 +357,6 @@ class CaptureService { const settleMs = Number(this.settings.get('capture.postHideSettleMs')); const run = async () => { if (!this.session || this.session.paused) { this.warmingUp = false; return; } - const startedAt = Date.now(); if (recorderWanted) { // Warm the recorder, but never let a slow backend start (it waits up // to several seconds for the capture stream) keep the window visible @@ -376,60 +370,18 @@ class CaptureService { if (capTimer) clearTimeout(capTimer); if (!this.session || this.session.paused) { this.warmingUp = false; return; } } - // Keep the window visible briefly so the user sees the transition even - // when warmup was instant; warmup time counts toward this. - const minVisibleMs = wantHide ? 400 : 0; - const elapsed = Date.now() - startedAt; - if (elapsed < minVisibleMs) { - await new Promise((r) => setTimeout(r, minVisibleMs - elapsed)); - if (!this.session || this.session.paused) { this.warmingUp = false; return; } - } if (wantHide && win && !win.isDestroyed() && win.isVisible()) { - if (!this.sessionInstructionsShown) { - this.sessionInstructionsShown = true; - await this.showRecordingInstructions(win); - if (!this.session || this.session.paused) { this.warmingUp = false; return; } - } win.hide(); // Let a couple of frames of the now-unobscured screen land before // the user's first click, so that frame shows their work, not the // app window that was just dismissed. await new Promise((r) => setTimeout(r, Number.isFinite(settleMs) ? settleMs : 150)); } - // Window hidden and buffer primed — clicks now count. - if (!process.env.STEPFORGE_SCREENSHOT && !this.sessionNotificationShown) { - try { - new Notification({ - title: 'StepForge is recording', - body: 'Use the red tray icon to pause or finish capture.', - }).show(); - this.sessionNotificationShown = true; - } catch { /* notifications unavailable on this desktop */ } - } this.warmingUp = false; }; run().catch(() => { this.warmingUp = false; }); } - async showRecordingInstructions(win) { - if (!this.dialog || typeof this.dialog.showMessageBox !== 'function') return; - try { - await this.dialog.showMessageBox(win, { - type: 'info', - title: 'StepForge recording', - message: 'Please go into the tray icon and select the red button to stop recording.', - detail: 'Click OK to continue and hide this window.', - buttons: ['OK'], - defaultId: 0, - cancelId: 0, - noLink: true, - }); - } catch { - // If the dialog cannot be shown, keep the session moving instead of - // leaving the user stuck on the start flow. - } - } - finishSession() { if (this.intervalTimer) { clearInterval(this.intervalTimer); @@ -441,8 +393,6 @@ class CaptureService { this.stopClickFrameBackend(); this.destroySessionTray(); this.session = null; - this.sessionNotificationShown = false; - this.sessionInstructionsShown = false; if (this.hiddenForSession) { this.hiddenForSession = false; this.showWindow(); diff --git a/app/main.js b/app/main.js index f8ff1c5..f929f09 100644 --- a/app/main.js +++ b/app/main.js @@ -672,7 +672,6 @@ if (!gotLock) { settings, getWindow: () => mainWindow, notify: sendToRenderer, - dialogApi: dialog, }); applyTheme(); diff --git a/app/renderer/app.js b/app/renderer/app.js index de97e3b..103c3c7 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -285,7 +285,13 @@ class StepForgeApp { const pauseBtn = el('button', { type: 'button', title: notStarted ? 'StepForge tucks away and starts capturing' : '', - onClick: () => send({ action: s.paused ? 'resume' : 'pause' }), + onClick: async () => { + if (notStarted) { + const acknowledged = await dialogs.showRecordingReminder(); + if (!acknowledged) return; + } + send({ action: s.paused ? 'resume' : 'pause' }); + }, }, notStarted ? 'Start recording' : s.paused ? 'Resume' : 'Pause'); const finishBtn = el('button', { diff --git a/app/renderer/dialogs.js b/app/renderer/dialogs.js index f037f5d..4bb4fe7 100644 --- a/app/renderer/dialogs.js +++ b/app/renderer/dialogs.js @@ -693,6 +693,27 @@ function showInfoDialog(title, bodyText) { }); } +function showRecordingReminder() { + return new Promise((resolve) => { + const { close } = openModal({ + title: 'Before recording starts', + body: el('div.recording-notice', {}, + el('div.recording-notice__badge', {}, 'Recording tip'), + el('div.recording-notice__title', {}, 'StepForge will hide after you continue.'), + el('div.recording-notice__text', {}, + 'When you want to pause or stop, use the red tray icon in the system tray.', + ), + ), + footer: [ + el('button.primary', { + onClick: () => { close(); resolve(true); }, + }, 'Continue'), + ], + onClose: () => resolve(false), + }); + }); +} + window.StepForgeDialogs = { promptText, showQuickActions, @@ -704,5 +725,6 @@ window.StepForgeDialogs = { showPlaceholdersDialog, showShortcutsDialog, showTemplateManager, + showRecordingReminder, }; })(); diff --git a/app/renderer/style.css b/app/renderer/style.css index 04a5457..7d8febd 100644 --- a/app/renderer/style.css +++ b/app/renderer/style.css @@ -619,6 +619,30 @@ fieldset legend { background: var(--warn); color: var(--text); } +.recording-notice { + display: grid; + gap: 10px; +} +.recording-notice__badge { + width: fit-content; + padding: 4px 10px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--panel-2); + color: var(--danger); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.recording-notice__title { + font-size: 18px; + font-weight: 700; +} +.recording-notice__text { + color: var(--muted); + line-height: 1.5; +} #modal-root:not(:empty) { position: fixed; diff --git a/tests/unit/capture.test.js b/tests/unit/capture.test.js index 52c5b4d..51323ca 100644 --- a/tests/unit/capture.test.js +++ b/tests/unit/capture.test.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict'); const CaptureService = require('../../app/capture'); -function makeService({ settings: settingsOverrides, screenApi, dialogApi } = {}) { +function makeService({ settings: settingsOverrides, screenApi } = {}) { const store = { addStep() { throw new Error('not used in this test'); @@ -26,9 +26,6 @@ function makeService({ settings: settingsOverrides, screenApi, dialogApi } = {}) settings, getWindow: () => null, notify: () => {}, - dialogApi: dialogApi || { - showMessageBox: async () => ({ response: 0 }), - }, screenApi: screenApi || { getCursorScreenPoint: () => ({ x: 0, y: 0 }), getAllDisplays: () => [], @@ -604,7 +601,7 @@ test('armRecording warms while visible, then hides and arms the session', async getBounds() { return { x: 0, y: 0, width: 800, height: 600 }; }, }; service.getWindow = () => win; - service.clickCaptureAvailable = () => true; + service.clickCaptureAvailable = () => false; // Stub the recorder so warmup resolves fast without real Electron. service.startClickFrameBackend = async () => {}; service.session = { guideId: 'g-arm', paused: false, count: 0, intervalSec: 0 }; @@ -629,52 +626,6 @@ test('armRecording warms while visible, then hides and arms the session', async service.finishSession(); }); -test('armRecording shows a blocking instruction dialog before the window hides', async () => { - const service = makeService(); - const win = { - destroyed: false, visible: true, - isDestroyed() { return this.destroyed; }, - isVisible() { return this.visible; }, - isMinimized() { return false; }, - hide() { this.visible = false; }, - show() { this.visible = true; }, - focus() {}, getTitle() { return 'StepForge'; }, - getBounds() { return { x: 0, y: 0, width: 800, height: 600 }; }, - }; - service.getWindow = () => win; - service.clickCaptureAvailable = () => false; - service.hiddenForSession = true; - service.session = { guideId: 'g-prompt', paused: true, count: 0, intervalSec: 0 }; - - let releaseDialog; - const dialogGate = new Promise((resolve) => { releaseDialog = resolve; }); - let seenOptions = null; - service.dialog = { - showMessageBox: async (_win, options) => { - seenOptions = options; - await dialogGate; - return { response: 0 }; - }, - }; - - service.togglePause(false); - - for (let i = 0; i < 40 && !seenOptions; i++) { - await new Promise((r) => setTimeout(r, 25)); - } - assert.ok(seenOptions, 'the instruction dialog must appear before the window hides'); - assert.equal(seenOptions?.message, 'Please go into the tray icon and select the red button to stop recording.'); - assert.equal(win.visible, true, 'the window must stay visible until the dialog is acknowledged'); - - releaseDialog(); - for (let i = 0; i < 20 && win.visible; i++) { - await new Promise((r) => setTimeout(r, 25)); - } - - assert.equal(win.visible, false, 'the window hides only after the acknowledgement dialog is dismissed'); - service.finishSession(); -}); - test('a slow recorder start still arms within the warmup cap', async () => { // If the backend start hangs (Windows can take seconds), the window must // still hide and recording must still arm — the restart bug was the @@ -1225,9 +1176,9 @@ test('a new session starts paused and does not hide the window until "Start reco // User clicks "Start recording" (the resume action). service.togglePause(false); assert.equal(service.session.paused, false); - assert.equal(win.hidden, 0, 'hide is deferred briefly so the user sees it happen'); + assert.equal(win.hidden, 0, 'hide is deferred until the resume path runs'); - await new Promise((r) => setTimeout(r, 450)); + await new Promise((r) => setTimeout(r, 25)); assert.equal(win.hidden, 1, 'window hides once recording actually starts'); } finally { service.finishSession(); From e46f01885b9899211f2bbc6c1b5229793bf5a36a Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:05:10 -0500 Subject: [PATCH 03/11] Prompt to start recording for library guides --- app/renderer/app.js | 27 ++++++++++++++++++++++----- app/renderer/dialogs.js | 9 ++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/renderer/app.js b/app/renderer/app.js index 103c3c7..5f4b1fa 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -191,8 +191,7 @@ class StepForgeApp { const guide = await api.library.create({ title: 'Untitled capture' }); await this.refreshData(); await this.openGuide(guide.guideId); - const state = await api.capture.session({ action: 'start', guideId: guide.guideId }); - this.updateCaptureState(state); + const state = await this.armCaptureSession(guide.guideId); const hotkey = this.state.settings?.capture?.hotkeyCapture; let how; if (state.clickCapture) { @@ -230,14 +229,26 @@ class StepForgeApp { this.renderTopbar(); } + // Start a paused session, optionally show a reminder, and continue once + // the user acknowledges it. + async armCaptureSession(guideId, reminder = null) { + const state = await api.capture.session({ action: 'start', guideId }); + this.updateCaptureState(state); + if (!reminder) return state; + const acknowledged = await dialogs.showRecordingReminder(reminder); + if (!acknowledged) return state; + const next = await api.capture.session({ action: 'resume', guideId }); + this.updateCaptureState(next); + return next; + } + // Opens a guide and arms (paused) capture for it, so the red REC bar pops // up right away with a "Start recording" option to resume capturing steps. async openGuideAndArmCapture(guideId, stepId = null) { await this.openGuide(guideId, stepId); // Don't restart (and reset the count of) a session already running for this guide. if (this.captureState?.active && this.captureState.guideId === guideId) return; - const state = await api.capture.session({ action: 'start', guideId }); - this.updateCaptureState(state); + await this.armCaptureSession(guideId); } onEditorMeta(meta) { @@ -767,7 +778,8 @@ class StepForgeApp { await this.refreshLibrary(); } - async createGuide() { + async createGuide({ armCapture = undefined } = {}) { + const shouldArmCapture = armCapture ?? this.state.view === 'library'; const title = await dialogs.promptText({ title: 'New Guide', label: 'Title', @@ -778,6 +790,11 @@ class StepForgeApp { const guide = await api.library.create({ title: title.trim() || 'Untitled guide' }); await this.refreshLibrary(); await this.openGuide(guide.guideId); + if (!shouldArmCapture) return; + await this.armCaptureSession(guide.guideId, { + actionLabel: 'Start recording', + headline: 'StepForge will hide after you start recording.', + }); } async createFolder() { diff --git a/app/renderer/dialogs.js b/app/renderer/dialogs.js index 4bb4fe7..2b03c1f 100644 --- a/app/renderer/dialogs.js +++ b/app/renderer/dialogs.js @@ -693,13 +693,16 @@ function showInfoDialog(title, bodyText) { }); } -function showRecordingReminder() { +function showRecordingReminder({ + actionLabel = 'Continue', + headline = 'StepForge will hide after you continue.', +} = {}) { return new Promise((resolve) => { const { close } = openModal({ title: 'Before recording starts', body: el('div.recording-notice', {}, el('div.recording-notice__badge', {}, 'Recording tip'), - el('div.recording-notice__title', {}, 'StepForge will hide after you continue.'), + el('div.recording-notice__title', {}, headline), el('div.recording-notice__text', {}, 'When you want to pause or stop, use the red tray icon in the system tray.', ), @@ -707,7 +710,7 @@ function showRecordingReminder() { footer: [ el('button.primary', { onClick: () => { close(); resolve(true); }, - }, 'Continue'), + }, actionLabel), ], onClose: () => resolve(false), }); From 1a0fda71f52756a691655e07c5d278a69ab25707 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:25:40 -0500 Subject: [PATCH 04/11] Remove new-capture follow-up toast --- app/renderer/app.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/renderer/app.js b/app/renderer/app.js index 5f4b1fa..fd7bd19 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -191,17 +191,7 @@ class StepForgeApp { const guide = await api.library.create({ title: 'Untitled capture' }); await this.refreshData(); await this.openGuide(guide.guideId); - const state = await this.armCaptureSession(guide.guideId); - const hotkey = this.state.settings?.capture?.hotkeyCapture; - let how; - if (state.clickCapture) { - how = 'every click will grab a step'; - } else if (state.intervalSec > 0) { - how = `a step will be grabbed every ${state.intervalSec}s`; - } else { - 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.`); + await this.armCaptureSession(guide.guideId); } async openExistingWorkspace() { From dfd0139dc551a67e77ceea174213dfd28e4ba941 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:30:54 -0500 Subject: [PATCH 05/11] Sync tests workflow with main --- .gitea/workflows/tests.yaml | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index eea9abf..39efead 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -10,34 +10,14 @@ on: jobs: tests: - runs-on: MOMP + runs-on: MOMP-AUTODOC steps: - name: Checkout repository uses: https://gitea.com/actions/checkout@v4 - - name: Install Electron system dependencies - run: | - apt-get update - apt-get install -y \ - xvfb \ - libnspr4 \ - libnss3 \ - libatk1.0-0 \ - libatk-bridge2.0-0 \ - libcups2 \ - libdrm2 \ - libgtk-3-0 \ - libx11-xcb1 \ - libxcomposite1 \ - libxdamage1 \ - libxrandr2 \ - libgbm1 \ - libxss1 \ - libasound2 - - name: Install dependencies - run: npm ci + run: npm ci --cache ~/.npm --prefer-offline - name: Run template tests env: From f0611a50b5630003daac0986acba12e8d1945923 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:35:20 -0500 Subject: [PATCH 06/11] Remove issue 4 capture toast --- app/renderer/editor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/renderer/editor.js b/app/renderer/editor.js index 310a086..6d986ee 100644 --- a/app/renderer/editor.js +++ b/app/renderer/editor.js @@ -1322,7 +1322,6 @@ class GuideEditor { async startCaptureSession() { await api.capture.session({ action: 'start', guideId: this.guideId }); - this.onToast('Capture session ready — click "Start recording" in the red bar when you\'re set.'); this.emitMeta(); } From c7038a151a3f608ac8761269b692e9e821a91c4e Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:40:57 -0500 Subject: [PATCH 07/11] Remove library auto-start and shoot button --- README.md | 2 +- app/main.js | 1 - app/renderer/app.js | 15 +-------------- docs/CHANGELOG.md | 6 +++--- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5012921..92c2073 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ The core workflow: active window, and region capture (the app hides itself during the shot), plus continuous capture sessions that grab a step on every click where the OS allows it, or on a 3/5/10 s auto-interval everywhere else (the REC bar - shows the live count with Shoot/Auto/Pause/Finish controls). Delay, global + shows the live count with Auto/Pause/Finish controls). Delay, global hotkeys, click markers, clipboard paste, and PNG/JPEG/GIF import included. The full keyboard shortcut list lives under **More ▾ → Keyboard shortcuts** in the editor. diff --git a/app/main.js b/app/main.js index f929f09..251d5bd 100644 --- a/app/main.js +++ b/app/main.js @@ -494,7 +494,6 @@ function setupIpc() { else if (action === 'resume') capture.togglePause(false); else if (action === 'finish') capture.finishSession(); else if (action === 'interval') capture.setInterval(intervalSec); - else if (action === 'shoot') await capture.sessionCapture('manual'); const state = capture.state(); sendToRenderer('capture:state', state); return state; diff --git a/app/renderer/app.js b/app/renderer/app.js index fd7bd19..9c4f40a 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -269,12 +269,6 @@ class StepForgeApp { : s.intervalSec > 0 ? `every ${s.intervalSec}s` : 'hotkey only'; - const shootBtn = el('button', { - type: 'button', - title: 'Capture a step now (the app hides itself for the shot)', - onClick: () => send({ action: 'shoot' }), - }, 'Shoot'); - // Cycle interval auto-capture: off -> 3s -> 5s -> 10s -> off. const nextInterval = { 0: 3, 3: 5, 5: 10, 10: 0 }[s.intervalSec ?? 0] ?? 3; const autoBtn = el('button', { @@ -302,7 +296,6 @@ class StepForgeApp { this.captureStatus.append( el('span', { title: `Capture session — ${trigger}` }, `REC ${s.count || 0} · ${trigger}`), - shootBtn, autoBtn, pauseBtn, finishBtn, @@ -768,8 +761,7 @@ class StepForgeApp { await this.refreshLibrary(); } - async createGuide({ armCapture = undefined } = {}) { - const shouldArmCapture = armCapture ?? this.state.view === 'library'; + async createGuide() { const title = await dialogs.promptText({ title: 'New Guide', label: 'Title', @@ -780,11 +772,6 @@ class StepForgeApp { const guide = await api.library.create({ title: title.trim() || 'Untitled guide' }); await this.refreshLibrary(); await this.openGuide(guide.guideId); - if (!shouldArmCapture) return; - await this.armCaptureSession(guide.guideId, { - actionLabel: 'Start recording', - headline: 'StepForge will hide after you start recording.', - }); } async createFolder() { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0ac8263..3d42067 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -102,8 +102,8 @@ Initial release. the platform supports it (xinput on X11, PowerShell on Windows), with interval auto-capture (3/5/10 s) as the always-works fallback when click detection or global hotkeys are unavailable (e.g. WSLg/Wayland). - The REC bar shows the live count and trigger, with Shoot / Auto / - Pause / Finish controls. + The REC bar shows the live count and trigger, with Auto / Pause / + Finish controls. - Recording sessions tuck the window away once and control everything from a red tray icon (capture now / pause / open / finish) instead of hiding the window for every shot — the app stays reachable @@ -161,7 +161,7 @@ Initial release. session now starts paused and the window only tucks away once the user presses "Start recording" in the capture bar, so the app doesn't vanish out from under you. -- The capture status bar (REC count / Shoot / Auto / Pause / Finish) is +- The capture status bar (REC count / Auto / Pause / Finish) is now shown only in the editor view; it no longer appears over the library when a session is still running in the background. - Click-triggered captures now grab the cursor position at the instant of From 4311225d50c6c0ee4e66472ac490bd01811d6c20 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:55:41 -0500 Subject: [PATCH 08/11] Remove auto and finish capture buttons --- README.md | 4 ++-- app/renderer/app.js | 15 --------------- docs/CHANGELOG.md | 6 +++--- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 92c2073..d6a105b 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ The core workflow: - **Capture engine** — the editor's **Capture ▾** button offers full screen, active window, and region capture (the app hides itself during the shot), plus continuous capture sessions that grab a step on every click where the - OS allows it, or on a 3/5/10 s auto-interval everywhere else (the REC bar - shows the live count with Auto/Pause/Finish controls). Delay, global + OS allows it, or on a 3/5/10 s auto-interval everywhere else. The REC bar + shows the live count and the start/pause control. Delay, global hotkeys, click markers, clipboard paste, and PNG/JPEG/GIF import included. The full keyboard shortcut list lives under **More ▾ → Keyboard shortcuts** in the editor. diff --git a/app/renderer/app.js b/app/renderer/app.js index 9c4f40a..61d6ddd 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -269,14 +269,6 @@ class StepForgeApp { : s.intervalSec > 0 ? `every ${s.intervalSec}s` : 'hotkey only'; - // Cycle interval auto-capture: off -> 3s -> 5s -> 10s -> off. - const nextInterval = { 0: 3, 3: 5, 5: 10, 10: 0 }[s.intervalSec ?? 0] ?? 3; - const autoBtn = el('button', { - type: 'button', - title: 'Automatically capture a step on a timer', - onClick: () => send({ action: 'interval', intervalSec: nextInterval }), - }, s.intervalSec > 0 ? `Auto ${s.intervalSec}s` : 'Auto off'); - const pauseBtn = el('button', { type: 'button', title: notStarted ? 'StepForge tucks away and starts capturing' : '', @@ -289,16 +281,9 @@ class StepForgeApp { }, }, notStarted ? 'Start recording' : s.paused ? 'Resume' : 'Pause'); - const finishBtn = el('button', { - type: 'button', - onClick: () => send({ action: 'finish' }), - }, 'Finish'); - this.captureStatus.append( el('span', { title: `Capture session — ${trigger}` }, `REC ${s.count || 0} · ${trigger}`), - autoBtn, pauseBtn, - finishBtn, ); } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3d42067..c85c546 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -102,8 +102,8 @@ Initial release. the platform supports it (xinput on X11, PowerShell on Windows), with interval auto-capture (3/5/10 s) as the always-works fallback when click detection or global hotkeys are unavailable (e.g. WSLg/Wayland). - The REC bar shows the live count and trigger, with Auto / Pause / - Finish controls. + The REC bar shows the live count and trigger, with the start/pause + control. - Recording sessions tuck the window away once and control everything from a red tray icon (capture now / pause / open / finish) instead of hiding the window for every shot — the app stays reachable @@ -161,7 +161,7 @@ Initial release. session now starts paused and the window only tucks away once the user presses "Start recording" in the capture bar, so the app doesn't vanish out from under you. -- The capture status bar (REC count / Auto / Pause / Finish) is +- The capture status bar (REC count / start/pause control) is now shown only in the editor view; it no longer appears over the library when a session is still running in the background. - Click-triggered captures now grab the cursor position at the instant of From 8c2229633f7be8d4c6a4c00387059da9f49b8df6 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:57:33 -0500 Subject: [PATCH 09/11] Simplify capture status label --- app/renderer/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/renderer/app.js b/app/renderer/app.js index 61d6ddd..e77c223 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -282,7 +282,7 @@ class StepForgeApp { }, notStarted ? 'Start recording' : s.paused ? 'Resume' : 'Pause'); this.captureStatus.append( - el('span', { title: `Capture session — ${trigger}` }, `REC ${s.count || 0} · ${trigger}`), + el('span', { title: `Capture session — ${trigger}` }, `Rec - ${trigger}`), pauseBtn, ); } From 356334dd3e4d6d762346e83a9732a37a89ec637e Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:58:52 -0500 Subject: [PATCH 10/11] Rename editor back button to library --- app/renderer/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/renderer/app.js b/app/renderer/app.js index e77c223..298a5aa 100644 --- a/app/renderer/app.js +++ b/app/renderer/app.js @@ -302,7 +302,7 @@ class StepForgeApp { const guide = this.editorMeta?.guide; this.topbarContext.append( - el('button', { type: 'button', onClick: () => this.showLibrary() }, 'Back'), + el('button', { type: 'button', onClick: () => this.showLibrary() }, 'Library'), el('button.primary', { type: 'button', title: 'Capture a screenshot step', From 6c2c837a836fd0ec2f36ec5704fa8e04df0143d8 Mon Sep 17 00:00:00 2001 From: Twest2 Date: Sat, 13 Jun 2026 14:59:27 -0500 Subject: [PATCH 11/11] Remove delayed capture menu item --- app/renderer/editor.js | 1 - docs/CHANGELOG.md | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/renderer/editor.js b/app/renderer/editor.js index 6d986ee..2d9aa10 100644 --- a/app/renderer/editor.js +++ b/app/renderer/editor.js @@ -1221,7 +1221,6 @@ class GuideEditor { { label: 'Capture full screen', action: () => this.captureStep('fullscreen') }, { label: 'Capture window', action: () => this.captureStep('window') }, { label: 'Capture region…', action: () => this.captureStep('region') }, - { label: 'Capture after 3 s delay', action: () => this.captureStep('fullscreen', 3000) }, 'sep', { label: 'Paste image as step', action: () => this.pasteClipboardStep() }, { label: 'Import images…', action: () => this.importImageSteps() }, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c85c546..4685d64 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -96,8 +96,8 @@ Initial release. Existing Workspace (guide library), and Settings. The brand button returns to the welcome screen from any view. - Capture menu in the editor topbar: full screen / window / region / - 3-second delay, paste image as step, import images, and capture - session start/finish — capture no longer requires the global hotkey. + paste image as step, import images, and capture session start/finish + — capture no longer requires the global hotkey. - Continuous capture sessions: steps are grabbed on every OS click where the platform supports it (xinput on X11, PowerShell on Windows), with interval auto-capture (3/5/10 s) as the always-works fallback when