Files
yellowjacket/.planning/phases/09-scan-cancellation-keyboard-shortcuts/09-01-PLAN.md
T

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
09-scan-cancellation-keyboard-shortcuts 01 execute 1
backend/events/events.go
frontend/src/events.ts
backend/library/library.go
backend/library/scan_control.go
backend/library/metrics.go
true
SCAN-01
SCAN-02
SCAN-03
truths artifacts key_links
CancelScan() cancels the scan context and workers stop at their next checkpoint
PauseScan() blocks workers via a channel; ResumeScan() unblocks them
Cancelled scans skip orphan cleanup to avoid deleting unvisited files
Batch commits use l.ctx (app context), not the cancellable scanCtx, so in-flight transactions complete
ScanMetrics.Cancelled is true when a scan was cancelled
path provides exports
backend/library/scan_control.go CancelScan, PauseScan, ResumeScan, IsScanActive, IsScanPaused methods
CancelScan
PauseScan
ResumeScan
IsScanActive
IsScanPaused
path provides contains
backend/events/events.go New scan control events LibraryScanCancelled
path provides contains
backend/library/metrics.go Cancelled field on ScanMetrics Cancelled
from to via pattern
backend/library/scan_control.go backend/library/library.go scanCancel context.CancelFunc and scanPauseCh channel on Library struct l.scanCancel|l.scanPauseCh
from to via pattern
backend/library/library.go backend/events/events.go EventsEmit for scan lifecycle events events.LibraryScan
Add scan cancellation and pause/resume to the Go backend. Thread a per-scan cancellable context through the existing scan pipeline, add pause/resume via a blocking channel, and expose Wails-bound methods for frontend control.

Purpose: Backend foundation for SCAN-01/02/03 — frontend buttons wire to these methods in Plan 03. Output: scan_control.go with CancelScan/PauseScan/ResumeScan, modified Scan() method, new events, updated metrics.

<execution_context> @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/09-scan-cancellation-keyboard-shortcuts/09-RESEARCH.md

@backend/library/library.go @backend/library/metrics.go @backend/events/events.go

type Library struct { mu sync.Mutex ctx context.Context logger *slog.Logger conf *Config db *database.DB rescanHooks RescanHooks }

type ScanMetrics struct { mu sync.Mutex // ... existing timing and count fields ... Added int64 json:"added" Updated int64 json:"updated" Skipped int64 json:"skipped" Removed int64 json:"removed" Warnings []ScanWarning json:"warnings" }

const ( LibraryScanStarted = "LibraryScanStarted" LibraryScanProgress = "LibraryScanProgress" LibraryScanComplete = "LibraryScanComplete" )

func (l *Library) Scan() (*ScanMetrics, error)

Task 1: Add scan control events and metrics fields backend/events/events.go, frontend/src/events.ts, backend/library/metrics.go 1. In `backend/events/events.go`, add a new const block for scan control events: ```go // Scan control events. const ( LibraryScanCancelled = "LibraryScanCancelled" LibraryScanPaused = "LibraryScanPaused" LibraryScanResumed = "LibraryScanResumed" ) ``` Place it after the existing Library events block (line 48).
  1. Run go generate ./backend/events/... to regenerate frontend/src/events.ts.

  2. In backend/library/metrics.go, add a Cancelled field to ScanMetrics:

    Cancelled bool `json:"cancelled"`
    

    Place it after the Removed int64 field (line 51), before the Warnings field. cd backend && go build ./... && go generate ./events/... && grep -q "LibraryScanCancelled" events/events.go && grep -q "LibraryScanCancelled" ../frontend/src/events.ts && grep -q "Cancelled" library/metrics.go Three new scan control events exist in events.go and are synced to frontend/src/events.ts. ScanMetrics has a Cancelled bool field.

Task 2: Add scan control fields to Library struct and create scan_control.go backend/library/library.go, backend/library/scan_control.go 1. In `backend/library/library.go`, add scan control fields to the `Library` struct (after `rescanHooks` at line 86): ```go // Scan control fields — protected by mu. scanActive bool scanCancel context.CancelFunc scanPaused bool scanPauseCh chan struct{} ```
  1. Create backend/library/scan_control.go with these Wails-bound methods:

    package library
    
    import (
        "github.com/wailsapp/wails/v2/pkg/runtime"
        "yellowjacket/backend/events"
    )
    
    // CancelScan cancels an in-progress scan. Returns immediately;
    // scan goroutines stop at their next checkpoint.
    func (l *Library) CancelScan() {
        l.mu.Lock()
        cancel := l.scanCancel
        l.mu.Unlock()
    
        if cancel != nil {
            cancel()
        }
    }
    
    // PauseScan pauses an in-progress scan. Workers block at their
    // next pause checkpoint until ResumeScan is called.
    func (l *Library) PauseScan() {
        l.mu.Lock()
        defer l.mu.Unlock()
    
        if !l.scanActive || l.scanPaused {
            return
        }
    
        l.scanPaused = true
        l.scanPauseCh = make(chan struct{})
    
        runtime.EventsEmit(l.ctx, events.LibraryScanPaused)
    }
    
    // ResumeScan unblocks a paused scan.
    func (l *Library) ResumeScan() {
        l.mu.Lock()
        defer l.mu.Unlock()
    
        if !l.scanPaused {
            return
        }
    
        l.scanPaused = false
        close(l.scanPauseCh) // unblocks all waiting workers
    
        runtime.EventsEmit(l.ctx, events.LibraryScanResumed)
    }
    
    // IsScanActive returns whether a scan is currently running.
    func (l *Library) IsScanActive() bool {
        l.mu.Lock()
        defer l.mu.Unlock()
        return l.scanActive
    }
    
    // IsScanPaused returns whether the scan is currently paused.
    func (l *Library) IsScanPaused() bool {
        l.mu.Lock()
        defer l.mu.Unlock()
        return l.scanPaused
    }
    
    // waitIfPaused blocks the calling goroutine if the scan is paused.
    // Returns ctx.Err() if the context is cancelled while waiting.
    func (l *Library) waitIfPaused(ctx context.Context) error {
        l.mu.Lock()
        ch := l.scanPauseCh
        paused := l.scanPaused
        l.mu.Unlock()
    
        if !paused || ch == nil {
            return nil
        }
    
        select {
        case <-ch:      // closed = unpaused
            return nil
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    

    Note: waitIfPaused takes a context.Context parameter (the scan-specific context), not l.ctx. Add "context" to the import block.

  2. Modify Scan() in backend/library/library.go:

    a. At the top of Scan() (after metrics := newScanMetrics(), line 176), create a cancellable scan context:

    scanCtx, scanCancel := context.WithCancel(l.ctx)
    defer scanCancel()
    
    l.mu.Lock()
    l.scanCancel = scanCancel
    l.scanActive = true
    l.scanPaused = false
    l.scanPauseCh = nil
    l.mu.Unlock()
    
    defer func() {
        l.mu.Lock()
        l.scanCancel = nil
        l.scanActive = false
        // If still paused, unpause so no dangling channel
        if l.scanPaused {
            l.scanPaused = false
            if l.scanPauseCh != nil {
                close(l.scanPauseCh)
            }
        }
        l.scanPauseCh = nil
        l.mu.Unlock()
    }()
    

    b. Replace ALL occurrences of <-l.ctx.Done() inside Scan() with <-scanCtx.Done(), and l.ctx.Err() with scanCtx.Err() (the walk goroutine send-to-workChan selects and the walk error return, and the worker pool send-to-resultChan select). There are 3 occurrences: line ~297, ~324, ~496.

    c. In the worker pool loop (Phase 3, around line 474), add a pause checkpoint before processing each file. Add at the start of the g.Go(func() error { closure body:

    if err := l.waitIfPaused(scanCtx); err != nil {
        return err
    }
    

    d. CRITICAL — Batch commits use l.ctx, NOT scanCtx: The commitBatch method and all DB operations within it should continue to use l.ctx (the app context), NOT the scan-specific scanCtx. This is already the case since commitBatch accesses l.ctx internally. DO NOT change commitBatch to use scanCtx. This ensures in-flight transactions always complete even when the scan is cancelled.

    e. CRITICAL — Skip orphan cleanup on cancelled scan: Before the orphan cleanup phase (Phase 5, around line 549), add a check:

    // Skip orphan cleanup if the scan was cancelled — existingPaths
    // still contains unvisited files that would be incorrectly deleted.
    cancelled := scanCtx.Err() != nil
    if cancelled {
        metrics.Cancelled = true
        l.logger.Info("scan cancelled, skipping orphan cleanup")
    } else {
        // ... existing orphan cleanup code ...
    }
    

    Wrap the existing orphan cleanup code (existingPaths.Range through metrics.OrphanCleanup = ...) inside the else block.

    f. Also skip the "Phase 6: post-scan variant generation" if cancelled (wrap in same if !cancelled check or separate check).

    g. When the scan was cancelled, emit LibraryScanCancelled instead of (or in addition to) LibraryScanComplete. Update the finalize section:

    if cancelled {
        runtime.EventsEmit(l.ctx, events.LibraryScanCancelled, metrics)
    } else {
        runtime.EventsEmit(l.ctx, events.LibraryScanComplete, metrics)
    }
    
cd backend && go build ./... && go vet ./library/... Library struct has scan control fields. scan_control.go provides CancelScan/PauseScan/ResumeScan/IsScanActive/IsScanPaused. Scan() uses per-scan context, workers check for pause, orphan cleanup is skipped on cancel, and appropriate events are emitted. ```bash cd backend && go build ./... && go vet ./library/... && go vet ./events/... ``` All backend code compiles. No vet errors. New scan control methods are exported and Wails-bindable.

<success_criteria>

  • go build ./... passes with no errors
  • CancelScan, PauseScan, ResumeScan, IsScanActive, IsScanPaused are exported methods on *Library
  • waitIfPaused is an unexported helper that blocks on pause channel
  • Scan() creates a per-scan context and uses it for worker cancellation
  • Orphan cleanup and variant generation are skipped when scan is cancelled
  • LibraryScanCancelled, LibraryScanPaused, LibraryScanResumed events exist and are synced to TypeScript
  • ScanMetrics.Cancelled bool field exists </success_criteria>
After completion, create `.planning/phases/09-scan-cancellation-keyboard-shortcuts/09-01-SUMMARY.md`