Phase 1 of the improvement plan (PR 3 of the sequence). The docs claimed
"fully offline"/"never talks to the network," but text-intel makes HTTP
requests to a configurable Ollama host that could be remote, with no timeout,
no cancellation, and no size limit; the Windows hook logged raw keystrokes
into capture metadata that could then be sent to that host.
Privacy — raw keystroke capture:
- New capture.captureTypedText setting, default false. With it off, printable
characters are never buffered in JS and the Windows keyboard hook never even
emits them across the process boundary (the flag is threaded into the C#).
Shortcut/navigation detection (Ctrl+T, Enter, …) is unaffected.
AI network hardening (app/text-intel.js):
- Every Ollama call goes through fetchJson with an AbortController deadline
(ai.timeoutMs, default 60s): a dead endpoint fails fast instead of leaving
UI actions pending forever.
- Cancellation: in-flight requests are tracked and cancelInflight(guideId)
aborts them; new ai:cancel IPC + api.ai.cancel are called when the editor
closes, and shutdown cancels everything.
- Bounded concurrency (2) for AI network work.
- Screenshots are only attached when allowed (ai.attachScreenshots), the model
is vision-capable, and the image is within ai.maxImageBytes — no more
unbounded base64-expanded 4K bodies.
Local-first host policy (core/text-intel.js):
- New isLoopbackHost + validateOllamaHost. By default only a loopback Ollama
endpoint is contacted; a remote host is refused with a clear message unless
ai.allowRemoteHost is explicitly enabled. Blocked hosts are never contacted.
Honest documentation:
- README, package.json, and the welcome screen drop "fully offline"/"never
talks to the network"/"Electron is the only dependency" for an accurate
local-first contract that discloses the optional AI path and the bundled
Tesseract OCR dependency.
- New docs/PRIVACY.md details exactly what is collected locally and the one
outbound (opt-in, loopback-by-default) AI feature.
Tests: loopback/remote host matrix, remote-blocked-without-opt-in (and never
contacted), remote-allowed-with-opt-in, request timeout, explicit cancel vs
timeout, typed-text off-by-default vs opted-in, shortcut detection still works,
and a source guard that the C# CHAR emission stays behind the opt-in. 224 unit
tests pass; startup smoke and workflow E2E pass.
Co-Authored-By: Claude Fable 5 <[email protected]>
Per request: clicks of the same button closer together than
capture.clickDebounceMs (default 200ms) now collapse into a single step, so
accidental fast/double clicks don't each become a step. It is a leading-edge
debounce measured from the last *accepted* click, so a run of fast clicks
can't push the next deliberate click out — two clicks spaced beyond the
window (e.g. the reported 400-500ms apart) always register.
Replaces the prior 8ms duplicate-delivery suppression (subsumed by the
window). Configurable; 0 captures every click.
Tests (the point of this change is that it can't silently regress):
- 13 behavioral unit tests in capture.test.js that drive real onOsClick
calls with controlled timestamps and assert which clicks survive — the
reported 400/450/500ms cases, sub-window collapse, the 200ms boundary,
per-button independence, configurability, debounce=0, last-accepted (not
last-dropped) reference, session reset, and a full onOsClick -> queue ->
store integration check. No keyword/comment assertions.
- A fourth end-to-end self-test scenario (burst of 40ms clicks collapses to
1; three 300ms-apart clicks each register => 4 total). The marker/drain
scenarios set debounce to 0 so they keep stressing the frame pipeline.
147 unit tests + all repo checks pass.
Co-Authored-By: Claude Fable 5 <[email protected]>
The first screenshot of a session was late while every later one was fine.
Cause: on 'Start recording' the window hid first and the capture backend
started warming up after — creating worker, getUserMedia, first frame takes
~1s. A click in that gap found no buffered frame and took the post-click
fresh shot.
armRecording() now warms the recorder while the window is still visible and
only hides once frames are buffering (with a brief post-hide settle so the
first frame shows the user's screen, not the dismissed app window). Verified
end to end with a new self-test scenario that clicks 250ms after start: the
first click is now served a pre-click frame instead of a post-click shot.
Co-Authored-By: Claude Fable 5 <[email protected]>
Real-world recording now saves every click with exact markers; the only
remaining nit was screenshots feeling a touch late. Add a configurable
click-lead (capture.clickLeadMs, default 120ms) that targets the screen
just before the hook timestamp, and tighten the stream sampling cadence to
50ms so a frame near that target always exists. Verified end to end: frames
now land ~120-160ms before the click (was 25-57ms), markers stay at 0.00%
offset, and the 8-click burst still saves all 8.
Also document the milestone in docs/CHANGELOG.md and remove an accidental
paste of Gitea commit-page text from it.
Co-Authored-By: Claude Fable 5 <[email protected]>
Implements the architecture change from ai_prompts/prompt3.md:
- New app/click-frames.js: shared timestamped frame ring + strict
click-to-frame pairing (never a frame whose grab started after the
click); legacy slack behavior kept behind capture.strictClickFrames=false.
- New stream capture backend (app/stream-backend.js + hidden worker
window): per-display desktop media streams sampled into ring buffers
and PNG-encoded entirely off the main process, so click delivery is
never starved by capture work. Auto-degrades to the legacy in-process
frame loop when streams cannot start or the worker stops answering.
- Clicks are paired with their frame at event time (eager pairing in
enqueueClickCapture); only the storing is serialized, so slow encodes
cannot skew later clicks in a fast burst.
- Linux watcher: restored event-time root coordinates from
xinput test-xi2 and merge raw/regular twin events structurally.
- Replaced the 40ms time debounce with source-aware duplicate
suppression: fast legitimate clicks are never dropped.
- New app/coords.js: physical-to-DIP conversion with multi-monitor and
scale-factor handling; Windows keeps screenToDipPoint.
- STEPFORGE_CLICK_SELFTEST end-to-end hook: 3/3 clicks become steps via
the stream backend with 0.00% marker offset on this host.
- Tests rewritten/added: strict selection, coords, stream backend,
Linux coordinate parsing, twin merge, burst clicking (126 passing).
Co-Authored-By: Claude Fable 5 <[email protected]>
The session previously only listened for the global hotkey, which is
unreliable under WSLg/Wayland — users got one screenshot and nothing
more. Sessions now layer three triggers:
- click-capture via OS adapters (xinput test-xi2 on X11, PowerShell
GetAsyncKeyState polling on Windows), debounced, ignoring clicks on
StepForge itself
- interval auto-capture (3/5/10 s) as the always-works fallback,
enabled by default when click detection is unavailable
- the existing global hotkey, plus a manual Shoot button
The REC bar now shows live count + active trigger with Shoot / Auto /
Pause / Finish. New captures and added steps are selected in the
editor (explicit reload(stepId) wins over a surviving selection).
Capture self-test hook (STEPFORGE_CAPTURE_SELFTEST) verifies 3x
hotkey-path captures and interval capture end-to-end.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Folder-based GuideStore with atomic writes, trash/restore, duplicate,
substep reparenting, folders/favorites, working-image crop/reset
- Allowlist HTML sanitizer applied on every store write
- Placeholder scopes (guide > global > system) and collection
- Persisted app settings with deep default merge
- 16 workflow tests exercising real on-disk round-trips
Co-Authored-By: Claude Fable 5 <[email protected]>