diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore index 27950ab..103cee8 100644 --- a/vscode-extension/.gitignore +++ b/vscode-extension/.gitignore @@ -1,5 +1,5 @@ node_modules/ dist/ *.vsix -!beetask-vscode-1.0.0.vsix +!beetask-vscode-*.vsix diff --git a/vscode-extension/README.md b/vscode-extension/README.md index 8985df4..4ce2879 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -7,6 +7,7 @@ Extensão oficial do **BeeTask** para o Visual Studio Code. Gerencie suas demand ## ✨ Recursos - **⏱️ Cronômetro em Tempo Real:** Acompanhe o tempo trabalhado na tarefa ativa direto na Barra de Status (rodapé do VS Code). +- **🛑 Pausa Automática por Inatividade (10min):** Se você se ausentar do VS Code por mais de 10 minutos com uma tarefa rodando, a extensão pausa o cronômetro automaticamente e pergunta se você ainda está trabalhando nela ao retornar. - **📋 Painel de Tarefas na Barra Lateral:** Visualize rapidamente suas tarefas pendentes, em execução e concluídas na aba BeeTask. - **▶️ Play / ⏸️ Pause com 1 Clique:** Inicie e pause o cronômetro das suas tarefas direto do VS Code. - **➕ Criação Rápida de Tarefas:** Crie novas tarefas associadas a projetos e categorias sem sair do editor. @@ -78,3 +79,5 @@ Acesse `Configurações` (`Ctrl+,`) e busque por `BeeTask`: - `beetask.serverUrl`: URL da API backend (Padrão: `https://api-task.beetech.dev`). - `beetask.webUrl`: URL do site web (Padrão: `https://task.beetech.dev`). - `beetask.refreshIntervalSeconds`: Intervalo de sincronização automática em segundos (Padrão: `30`). +- `beetask.enableIdleDetection`: Ativar pausa automática da tarefa por inatividade (Padrão: `true`). +- `beetask.idleTimeoutMinutes`: Tempo limite de inatividade em minutos (Padrão: `10`). diff --git a/vscode-extension/beetask-vscode-1.0.0.vsix b/vscode-extension/beetask-vscode-1.0.0.vsix deleted file mode 100644 index 2edc8b2..0000000 Binary files a/vscode-extension/beetask-vscode-1.0.0.vsix and /dev/null differ diff --git a/vscode-extension/beetask-vscode-1.1.0.vsix b/vscode-extension/beetask-vscode-1.1.0.vsix new file mode 100644 index 0000000..79484dc Binary files /dev/null and b/vscode-extension/beetask-vscode-1.1.0.vsix differ diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 69a4fad..1ba04d5 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "beetask-vscode", "displayName": "BeeTask", "description": "Gerenciamento e rastreamento de tarefas com cronômetro integrado ao VS Code", - "version": "1.0.0", + "version": "1.1.0", "publisher": "beetech", "repository": { "type": "git", @@ -153,6 +153,16 @@ "type": "number", "default": 30, "description": "Intervalo de atualização automática das tarefas (em segundos)" + }, + "beetask.enableIdleDetection": { + "type": "boolean", + "default": true, + "description": "Ativar pausa automática da tarefa por inatividade no VS Code" + }, + "beetask.idleTimeoutMinutes": { + "type": "number", + "default": 10, + "description": "Tempo limite de inatividade em minutos para pausar a tarefa ativa (padrão: 10 minutos)" } } } diff --git a/vscode-extension/src/commands.ts b/vscode-extension/src/commands.ts index 28e15cd..eb19bf4 100644 --- a/vscode-extension/src/commands.ts +++ b/vscode-extension/src/commands.ts @@ -3,6 +3,7 @@ import { BeeTaskApiClient } from './api'; import { AuthManager } from './authManager'; import { StatusBarManager } from './statusBarManager'; import { TaskTreeProvider, TaskTreeItem } from './taskTreeProvider'; +import { IdleDetector } from './idleDetector'; import { Task } from './types'; export function registerCommands( @@ -10,7 +11,8 @@ export function registerCommands( api: BeeTaskApiClient, authManager: AuthManager, statusBar: StatusBarManager, - treeProvider: TaskTreeProvider + treeProvider: TaskTreeProvider, + idleDetector?: IdleDetector ): void { // 1. LOGIN context.subscriptions.push( @@ -56,7 +58,7 @@ export function registerCommands( } ); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); // Prompt org selection if multiple const orgs = authManager.getOrganizations(); @@ -109,7 +111,7 @@ export function registerCommands( if (selected) { await authManager.setActiveOrgId(selected.orgId); vscode.window.showInformationMessage(`Organização ativa alterada para: ${selected.label}`); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); } }) ); @@ -117,7 +119,7 @@ export function registerCommands( // 4. REFRESH TASKS context.subscriptions.push( vscode.commands.registerCommand('beetask.refreshTasks', async () => { - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); vscode.window.setStatusBarMessage('BeeTask: Tarefas atualizadas.', 2000); }) ); @@ -155,7 +157,7 @@ export function registerCommands( try { await api.startTask(task.id); vscode.window.showInformationMessage(`▶️ Iniciada: #${task.id} ${task.title}`); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); } catch (err: any) { vscode.window.showErrorMessage(`Falha ao iniciar tarefa: ${err.message}`); } @@ -182,7 +184,7 @@ export function registerCommands( try { await api.stopTask(task.id); vscode.window.showInformationMessage(`⏸️ Pausada: #${task.id} ${task.title}`); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); } catch (err: any) { vscode.window.showErrorMessage(`Falha ao pausar tarefa: ${err.message}`); } @@ -215,7 +217,7 @@ export function registerCommands( try { await api.updateStatus(task.id, 'COMPLETED'); vscode.window.showInformationMessage(`✔️ Tarefa #${task.id} concluída com sucesso!`); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); } catch (err: any) { vscode.window.showErrorMessage(`Falha ao concluir tarefa: ${err.message}`); } @@ -279,7 +281,7 @@ export function registerCommands( }); vscode.window.showInformationMessage(`Tarefa #${created.id} criada com sucesso!`); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); const startNow = await vscode.window.showInformationMessage( `Deseja iniciar o cronômetro para #${created.id} agora?`, @@ -288,7 +290,7 @@ export function registerCommands( ); if (startNow === 'Sim, Iniciar') { await api.startTask(created.id); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); } } catch (err: any) { vscode.window.showErrorMessage(`Falha ao criar tarefa: ${err.message}`); @@ -361,7 +363,7 @@ export function registerCommands( }); vscode.window.showInformationMessage(`✨ Tarefa #${created.id} criada via IA!`); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); const startNow = await vscode.window.showInformationMessage( `Deseja iniciar o cronômetro para #${created.id}?`, @@ -370,7 +372,7 @@ export function registerCommands( ); if (startNow === 'Sim, Iniciar') { await api.startTask(created.id); - await refreshAll(api, authManager, statusBar, treeProvider); + await refreshAll(api, authManager, statusBar, treeProvider, idleDetector); } } catch (err: any) { vscode.window.showErrorMessage(`Falha na criação com IA: ${err.message}`); @@ -526,8 +528,10 @@ async function refreshAll( api: BeeTaskApiClient, authManager: AuthManager, statusBar: StatusBarManager, - treeProvider: TaskTreeProvider + treeProvider: TaskTreeProvider, + idleDetector?: IdleDetector ): Promise { + idleDetector?.recordActivity(); const isAuth = await authManager.isAuthenticated(); if (!isAuth) { statusBar.setActiveTask(null); diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 3cb97dc..7606695 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -3,6 +3,7 @@ import { AuthManager } from './authManager'; import { BeeTaskApiClient } from './api'; import { StatusBarManager } from './statusBarManager'; import { TaskTreeProvider } from './taskTreeProvider'; +import { IdleDetector } from './idleDetector'; import { registerCommands } from './commands'; let pollTimer: NodeJS.Timeout | null = null; @@ -12,17 +13,19 @@ export async function activate(context: vscode.ExtensionContext): Promise const api = new BeeTaskApiClient(authManager); const statusBar = new StatusBarManager(); const treeProvider = new TaskTreeProvider(api, authManager); + const idleDetector = new IdleDetector(api, authManager, statusBar, treeProvider); - // Register Tree View + // Register Tree View & Status Bar & Idle Detector const treeView = vscode.window.createTreeView('beetask.tasksView', { treeDataProvider: treeProvider, showCollapseAll: true, }); context.subscriptions.push(treeView); context.subscriptions.push(statusBar); + context.subscriptions.push(idleDetector); // Register Commands - registerCommands(context, api, authManager, statusBar, treeProvider); + registerCommands(context, api, authManager, statusBar, treeProvider, idleDetector); // Initial Sync if (await authManager.isAuthenticated()) { diff --git a/vscode-extension/src/idleDetector.ts b/vscode-extension/src/idleDetector.ts new file mode 100644 index 0000000..c2d1361 --- /dev/null +++ b/vscode-extension/src/idleDetector.ts @@ -0,0 +1,168 @@ +import * as vscode from 'vscode'; +import { BeeTaskApiClient } from './api'; +import { AuthManager } from './authManager'; +import { StatusBarManager } from './statusBarManager'; +import { TaskTreeProvider } from './taskTreeProvider'; +import { Task } from './types'; + +export class IdleDetector implements vscode.Disposable { + private lastActivityTime: number = Date.now(); + private checkInterval: NodeJS.Timeout | null = null; + private isPrompting: boolean = false; + private disposables: vscode.Disposable[] = []; + + private api: BeeTaskApiClient; + private authManager: AuthManager; + private statusBar: StatusBarManager; + private treeProvider: TaskTreeProvider; + + constructor( + api: BeeTaskApiClient, + authManager: AuthManager, + statusBar: StatusBarManager, + treeProvider: TaskTreeProvider + ) { + this.api = api; + this.authManager = authManager; + this.statusBar = statusBar; + this.treeProvider = treeProvider; + + this.registerActivityListeners(); + this.startPeriodicCheck(); + } + + public recordActivity(): void { + this.lastActivityTime = Date.now(); + } + + private registerActivityListeners(): void { + // 1. Digitação ou alteração de código + this.disposables.push( + vscode.workspace.onDidChangeTextDocument(() => this.recordActivity()) + ); + + // 2. Movimentação de cursor ou seleção de texto + this.disposables.push( + vscode.window.onDidChangeTextEditorSelection(() => this.recordActivity()) + ); + + // 3. Troca de arquivo / aba ativa + this.disposables.push( + vscode.window.onDidChangeActiveTextEditor(() => this.recordActivity()) + ); + + // 4. Rolagem da página (scroll) + this.disposables.push( + vscode.window.onDidChangeTextEditorVisibleRanges(() => this.recordActivity()) + ); + + // 5. Salvamento de arquivo + this.disposables.push( + vscode.workspace.onDidSaveTextDocument(() => this.recordActivity()) + ); + + // 6. Janela do VS Code ganhou foco + this.disposables.push( + vscode.window.onDidChangeWindowState((state) => { + if (state.focused) { + this.recordActivity(); + } + }) + ); + } + + private startPeriodicCheck(): void { + // Checa a cada 15 segundos + this.checkInterval = setInterval(async () => { + await this.checkIdleStatus(); + }, 15000); + } + + private async checkIdleStatus(): Promise { + const config = vscode.workspace.getConfiguration('beetask'); + const enabled = config.get('enableIdleDetection', true); + const timeoutMinutes = config.get('idleTimeoutMinutes', 10); + + if (!enabled || timeoutMinutes <= 0) { + return; + } + + const activeTask = this.statusBar.getActiveTask(); + if (!activeTask) { + // Se não há tarefa rodando, mantém o relógio zerado para a próxima + this.lastActivityTime = Date.now(); + return; + } + + if (this.isPrompting) { + return; + } + + const idleMillis = Date.now() - this.lastActivityTime; + const idleMinutes = idleMillis / 60000; + + if (idleMinutes >= timeoutMinutes) { + await this.handleIdlePause(activeTask, timeoutMinutes); + } + } + + private async handleIdlePause(task: Task, timeoutMinutes: number): Promise { + this.isPrompting = true; + console.log(`[BeeTask] Inatividade de ${timeoutMinutes}min detectada. Pausando tarefa #${task.id}...`); + + try { + // 1. Pausa imediatamente a tarefa no servidor + await this.api.stopTask(task.id); + this.statusBar.setActiveTask(null); + this.treeProvider.refresh(); + + // 2. Pergunta ao usuário se ele ainda está trabalhando nela + const question = `Você ainda está trabalhando na tarefa #${task.id} - ${task.title}?`; + const choice = await vscode.window.showWarningMessage( + question, + { modal: false }, + 'Sim', + 'Não' + ); + + if (choice === 'Sim') { + // Usuário confirmou que ainda está trabalhando -> retoma a tarefa + await this.api.startTask(task.id); + vscode.window.showInformationMessage(`▶️ Tarefa #${task.id} retomada!`); + } else { + // Clicou "Não" ou fechou a notificação -> mantém pausada + vscode.window.showInformationMessage(`⏸️ Tarefa #${task.id} mantida pausada.`); + } + } catch (err: any) { + console.error('[BeeTask] Erro ao processar pausa por inatividade:', err); + } finally { + // Reinicia o relógio de atividade para não dar loop + this.recordActivity(); + this.isPrompting = false; + + // Sincroniza estado visual final + try { + const orgId = this.authManager.getActiveOrgId(); + const tasks = await this.api.listMyTasks(orgId); + let runningTask = null; + for (const t of tasks) { + if (t.history && t.history.some(h => h.startAt && !h.endAt)) { + runningTask = t; + break; + } + } + this.statusBar.setActiveTask(runningTask); + this.treeProvider.refresh(); + } catch (_) {} + } + } + + public dispose(): void { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + this.disposables.forEach(d => d.dispose()); + this.disposables = []; + } +}