diff --git a/CHANGELOG.md b/CHANGELOG.md index d51a7db..5b9c92f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Update .vscodeignore to exclude internal scripts/tests/CI and add Content-Security-Policy to sidebar webview (Fixes #29). - Fix pet mood duration and unlock typing combo meter in Feed/Play commands (Fixes #23). - Fix accessory positioning and eye-blink transform-origins for Cat and Robot pet species (Fixes #27). +- Automatically wake pet and reset mood from sleepy/bored when refocusing VS Code or switching active editors after system sleep (Fixes #11). --- diff --git a/extension.js b/extension.js index af08a68..4465e0f 100644 --- a/extension.js +++ b/extension.js @@ -413,6 +413,35 @@ function activate(context) { } }); + // ── Reset Idle Mood on Activity / Focus / Wake ── + function resetIdleMood() { + lastActivity = Date.now(); + if (!flashTimeout && (currentMood === 'sleepy' || currentMood === 'bored')) { + currentMood = prevErrorCount > 0 ? 'struggling' : 'happy'; + _applyMood(currentMood); + updateDashboardIfOpen(stats, petName(), currentMood); + sidebarProvider.update(); + } + } + + // ── Event: Active Editor Changed ── + const onEditorChange = typeof vscode.window.onDidChangeActiveTextEditor === 'function' + ? vscode.window.onDidChangeActiveTextEditor(editor => { + if (editor) { + resetIdleMood(); + } + }) + : { dispose() {} }; + + // ── Event: Window Focus / System Wake ── + const onWindowStateChange = typeof vscode.window.onDidChangeWindowState === 'function' + ? vscode.window.onDidChangeWindowState(state => { + if (state && state.focused) { + resetIdleMood(); + } + }) + : { dispose() {} }; + // ── Idle Mood Timer (every 30 s) ── idleTimer = setInterval(() => { if (flashTimeout) return; @@ -488,6 +517,8 @@ function activate(context) { onSave, onChange, onDiagnostics, + onEditorChange, + onWindowStateChange, onConfigChange, { dispose: () => { clearInterval(idleTimer); diff --git a/tests/sleep.test.js b/tests/sleep.test.js new file mode 100644 index 0000000..bfff534 --- /dev/null +++ b/tests/sleep.test.js @@ -0,0 +1,153 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const vm = require('node:vm'); + +test('Issue #11: Resets pet mood from sleepy/bored on active editor change or window focus', (t, done) => { + const disposable = { dispose() {} }; + let editorChangeHandler = null; + let windowStateChangeHandler = null; + let idleTimerCallback = null; + + const statusBar = { + text: '', + tooltip: '', + show() {}, + hide() {} + }; + + const vscode = { + DiagnosticSeverity: { Error: 0 }, + StatusBarAlignment: { Right: 1 }, + MarkdownString: class { constructor(value) { this.value = value; } }, + commands: { + registerCommand: () => disposable + }, + window: { + createStatusBarItem: () => statusBar, + registerWebviewViewProvider: () => disposable, + showInformationMessage: () => Promise.resolve(), + visibleTextEditors: [], + onDidChangeActiveTextEditor: handler => { + editorChangeHandler = handler; + return disposable; + }, + onDidChangeWindowState: handler => { + windowStateChangeHandler = handler; + return disposable; + } + }, + workspace: { + getConfiguration: () => ({ get: (key, fallback) => key === 'weeklyReview' ? false : fallback }), + onDidSaveTextDocument: () => disposable, + onDidChangeTextDocument: () => disposable, + onDidChangeConfiguration: () => disposable + }, + languages: { + onDidChangeDiagnostics: () => disposable, + getDiagnostics: () => [] + } + }; + + let sidebarUpdated = 0; + class MockSidebarProvider { + constructor() {} + update() { sidebarUpdated++; } + } + + let dashboardUpdatedMood = null; + const dependencies = { + vscode, + './src/stats': { + StatsEngine: class { + getLevel() { return 1; } + getSnapshot() { return { level: 1, saveCount: 0, linesTyped: 0, currentStreak: 0, errorsFixed: 0 }; } + } + }, + './src/moods': { + getMoodDisplay: mood => ({ statusText: `(${mood})`, label: mood, tip: '' }), + checkMilestones: () => [], + getRandomHiMessage: () => 'hi', + getRandomTip: () => 'tip', + PET_THEMES: { default: { label: 'Default' } } + }, + './src/dashboard': { + showDashboard: () => {}, + updateDashboardIfOpen: (stats, name, mood) => { dashboardUpdatedMood = mood; } + }, + './src/sidebar': { + SidebarProvider: MockSidebarProvider + } + }; + + let currentTime = 1000000; + const sandbox = { + module: { exports: {} }, + require: name => dependencies[name] || require(name), + __dirname: path.join(__dirname, '..'), + setTimeout: (fn) => setTimeout(fn, 0), + clearTimeout, + setInterval: (fn, ms) => { + if (ms === 30000) { + idleTimerCallback = fn; + } + return 1; + }, + clearInterval: () => {}, + Date: { + now: () => currentTime + } + }; + + const extensionCode = fs.readFileSync(path.join(__dirname, '..', 'extension.js'), 'utf8'); + vm.runInNewContext(extensionCode, sandbox); + const activate = sandbox.module.exports.activate; + const deactivate = sandbox.module.exports.deactivate; + + const mockContext = { + subscriptions: [], + globalState: { get: () => null, update: () => Promise.resolve() } + }; + + activate(mockContext); + + assert.ok(typeof editorChangeHandler === 'function', 'onDidChangeActiveTextEditor handler must be registered'); + assert.ok(typeof windowStateChangeHandler === 'function', 'onDidChangeWindowState handler must be registered'); + + // Simulate idle for 15 minutes (system sleep) + currentTime += 15 * 60 * 1000; + assert.ok(typeof idleTimerCallback === 'function', 'idleTimerCallback registered'); + idleTimerCallback(); + + assert.ok(statusBar.text.includes('sleepy'), 'pet must be sleepy after 15m idle'); + + // Case 1: Switching active text editor wakes pet up + sidebarUpdated = 0; + editorChangeHandler({ document: {} }); + assert.ok(statusBar.text.includes('happy'), 'active editor change must wake pet from sleepy to happy'); + assert.equal(dashboardUpdatedMood, 'happy', 'dashboard updated to happy'); + assert.ok(sidebarUpdated > 0, 'sidebar updated on wake'); + + // Simulate idle again to bored + currentTime += 5 * 60 * 1000; + idleTimerCallback(); + assert.ok(statusBar.text.includes('bored'), 'pet must be bored after 5m idle'); + + // Case 2: Window gain focus (system wake / return to VS Code) wakes pet up + sidebarUpdated = 0; + windowStateChangeHandler({ focused: true }); + assert.ok(statusBar.text.includes('happy'), 'window focus must wake pet from bored to happy'); + + // Case 3: Window blur does NOT wake pet up + currentTime += 12 * 60 * 1000; + idleTimerCallback(); + assert.ok(statusBar.text.includes('sleepy'), 'pet must be sleepy again'); + windowStateChangeHandler({ focused: false }); + assert.ok(statusBar.text.includes('sleepy'), 'window blur should not alter sleepy state'); + + deactivate(); + done(); +});