feat(extension): adicionar deteccao de inatividade com auto-pause e confirmacao de retorno

This commit is contained in:
Gabriel Menezes 2026-11-05 11:50:44 -04:00
parent e445ad565b
commit 08d735f61a
8 changed files with 204 additions and 16 deletions

View file

@ -1,5 +1,5 @@
node_modules/
dist/
*.vsix
!beetask-vscode-1.0.0.vsix
!beetask-vscode-*.vsix

View file

@ -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`).

Binary file not shown.

View file

@ -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)"
}
}
}

View file

@ -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<void> {
idleDetector?.recordActivity();
const isAuth = await authManager.isAuthenticated();
if (!isAuth) {
statusBar.setActiveTask(null);

View file

@ -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<void>
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()) {

View file

@ -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<void> {
const config = vscode.workspace.getConfiguration('beetask');
const enabled = config.get<boolean>('enableIdleDetection', true);
const timeoutMinutes = config.get<number>('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<void> {
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 = [];
}
}