beetask/vscode-extension/src/commands.ts

560 lines
19 KiB
TypeScript

import * as vscode from 'vscode';
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(
context: vscode.ExtensionContext,
api: BeeTaskApiClient,
authManager: AuthManager,
statusBar: StatusBarManager,
treeProvider: TaskTreeProvider,
idleDetector?: IdleDetector
): void {
// 1. LOGIN
context.subscriptions.push(
vscode.commands.registerCommand('beetask.login', async () => {
const config = vscode.workspace.getConfiguration('beetask');
const defaultUrl = config.get<string>('serverUrl', 'https://api-task.beetech.dev');
const serverUrl = await vscode.window.showInputBox({
prompt: 'URL da API do BeeTask',
value: defaultUrl,
ignoreFocusOut: true,
});
if (!serverUrl) return;
if (serverUrl !== defaultUrl) {
await config.update('serverUrl', serverUrl, vscode.ConfigurationTarget.Global);
}
const email = await vscode.window.showInputBox({
prompt: 'Email de acesso ao BeeTask',
placeHolder: 'seu.email@empresa.com',
ignoreFocusOut: true,
});
if (!email) return;
const password = await vscode.window.showInputBox({
prompt: 'Sua senha',
password: true,
ignoreFocusOut: true,
});
if (!password) return;
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Autenticando no BeeTask...',
cancellable: false,
},
async () => {
const loginRes = await api.login(email.trim(), password);
vscode.window.showInformationMessage(`Conectado com sucesso como ${loginRes.name || loginRes.email}!`);
}
);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
// Prompt org selection if multiple
const orgs = authManager.getOrganizations();
if (orgs.length > 1) {
await vscode.commands.executeCommand('beetask.selectOrg');
}
} catch (err: any) {
vscode.window.showErrorMessage(`Falha no login: ${err.message}`);
}
})
);
// 2. LOGOUT
context.subscriptions.push(
vscode.commands.registerCommand('beetask.logout', async () => {
const confirm = await vscode.window.showWarningMessage(
'Deseja realmente desconectar sua conta do BeeTask?',
'Sim, Desconectar',
'Cancelar'
);
if (confirm === 'Sim, Desconectar') {
await authManager.clearAuth();
statusBar.setActiveTask(null);
treeProvider.refresh();
vscode.window.showInformationMessage('Conta desconectada do BeeTask.');
}
})
);
// 3. SELECT ORGANIZATION
context.subscriptions.push(
vscode.commands.registerCommand('beetask.selectOrg', async () => {
const orgs = authManager.getOrganizations();
if (!orgs || orgs.length === 0) {
vscode.window.showInformationMessage('Nenhuma organização encontrada.');
return;
}
const activeId = authManager.getActiveOrgId();
const items = orgs.map(o => ({
label: o.name,
description: o.id === activeId ? '(Ativa)' : '',
orgId: o.id,
}));
const selected = await vscode.window.showQuickPick(items, {
placeHolder: 'Selecione a Organização ativa',
});
if (selected) {
await authManager.setActiveOrgId(selected.orgId);
vscode.window.showInformationMessage(`Organização ativa alterada para: ${selected.label}`);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
}
})
);
// 4. REFRESH TASKS
context.subscriptions.push(
vscode.commands.registerCommand('beetask.refreshTasks', async () => {
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
vscode.window.setStatusBarMessage('BeeTask: Tarefas atualizadas.', 2000);
})
);
// 5. START TASK (PLAY)
context.subscriptions.push(
vscode.commands.registerCommand('beetask.startTask', async (arg?: Task | TaskTreeItem) => {
let task: Task | undefined;
if (arg && 'task' in arg && arg.task) {
task = arg.task;
} else if (arg && 'id' in arg) {
task = arg as Task;
}
if (!task) {
// Show pick of pending tasks
const tasks = treeProvider.getTasks().filter(t => t.status !== 'COMPLETED');
if (tasks.length === 0) {
vscode.window.showInformationMessage('Nenhuma tarefa pendente encontrada.');
return;
}
const picked = await vscode.window.showQuickPick(
tasks.map(t => ({
label: `#${t.id} ${t.title}`,
description: t.project?.name || '',
task: t,
})),
{ placeHolder: 'Selecione a tarefa para iniciar o cronômetro' }
);
if (!picked) return;
task = picked.task;
}
try {
await api.startTask(task.id);
vscode.window.showInformationMessage(`▶️ Iniciada: #${task.id} ${task.title}`);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
} catch (err: any) {
vscode.window.showErrorMessage(`Falha ao iniciar tarefa: ${err.message}`);
}
})
);
// 6. STOP TASK (PAUSE)
context.subscriptions.push(
vscode.commands.registerCommand('beetask.stopTask', async (arg?: Task | TaskTreeItem) => {
let task: Task | null = null;
if (arg && 'task' in arg && arg.task) {
task = arg.task;
} else if (arg && 'id' in arg) {
task = arg as Task;
} else {
task = statusBar.getActiveTask();
}
if (!task) {
vscode.window.showInformationMessage('Nenhuma tarefa em execução para pausar.');
return;
}
try {
await api.stopTask(task.id);
vscode.window.showInformationMessage(`⏸️ Pausada: #${task.id} ${task.title}`);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
} catch (err: any) {
vscode.window.showErrorMessage(`Falha ao pausar tarefa: ${err.message}`);
}
})
);
// 7. COMPLETE TASK
context.subscriptions.push(
vscode.commands.registerCommand('beetask.completeTask', async (arg?: Task | TaskTreeItem) => {
let task: Task | undefined;
if (arg && 'task' in arg && arg.task) {
task = arg.task;
} else if (arg && 'id' in arg) {
task = arg as Task;
} else {
const active = statusBar.getActiveTask();
if (active) task = active;
}
if (!task) {
const tasks = treeProvider.getTasks().filter(t => t.status !== 'COMPLETED');
const picked = await vscode.window.showQuickPick(
tasks.map(t => ({ label: `#${t.id} ${t.title}`, task: t })),
{ placeHolder: 'Selecione a tarefa a ser concluída' }
);
if (!picked) return;
task = picked.task;
}
try {
await api.updateStatus(task.id, 'COMPLETED');
vscode.window.showInformationMessage(`✔️ Tarefa #${task.id} concluída com sucesso!`);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
} catch (err: any) {
vscode.window.showErrorMessage(`Falha ao concluir tarefa: ${err.message}`);
}
})
);
// 8. CREATE TASK (STANDARD)
context.subscriptions.push(
vscode.commands.registerCommand('beetask.createTask', async () => {
const orgId = authManager.getActiveOrgId();
if (!orgId) {
vscode.window.showWarningMessage('Selecione uma organização primeiro.');
return;
}
const title = await vscode.window.showInputBox({
prompt: 'Título da Nova Tarefa',
placeHolder: 'Ex: Desenvolver componente de busca',
ignoreFocusOut: true,
});
if (!title?.trim()) return;
const description = await vscode.window.showInputBox({
prompt: 'Descrição (opcional)',
placeHolder: 'Critérios de aceitação, notas ou escopo...',
});
// Optional project selection
let selectedProjectId: number | undefined;
const projects = await api.listProjects(orgId);
if (projects.length > 0) {
const projPick = await vscode.window.showQuickPick(
[{ label: '(Nenhum projeto)', id: undefined }, ...projects.map(p => ({ label: p.name, id: p.id }))],
{ placeHolder: 'Vincular a um Projeto? (opcional)' }
);
if (projPick && projPick.id) {
selectedProjectId = projPick.id;
}
}
// Optional category selection
let selectedCategoryId: number | undefined;
const categories = await api.listCategories(orgId);
if (categories.length > 0) {
const catPick = await vscode.window.showQuickPick(
[{ label: '(Nenhuma categoria)', id: undefined }, ...categories.map(c => ({ label: c.name, id: c.id }))],
{ placeHolder: 'Vincular a uma Categoria? (opcional)' }
);
if (catPick && catPick.id) {
selectedCategoryId = catPick.id;
}
}
try {
const created = await api.createTask({
title: title.trim(),
description: description?.trim() || undefined,
organizationId: orgId,
projectId: selectedProjectId,
categoryId: selectedCategoryId,
});
vscode.window.showInformationMessage(`Tarefa #${created.id} criada com sucesso!`);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
const startNow = await vscode.window.showInformationMessage(
`Deseja iniciar o cronômetro para #${created.id} agora?`,
'Sim, Iniciar',
'Não'
);
if (startNow === 'Sim, Iniciar') {
await api.startTask(created.id);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
}
} catch (err: any) {
vscode.window.showErrorMessage(`Falha ao criar tarefa: ${err.message}`);
}
})
);
// 9. CREATE TASK WITH AI
context.subscriptions.push(
vscode.commands.registerCommand('beetask.createTaskAi', async () => {
const orgId = authManager.getActiveOrgId();
if (!orgId) {
vscode.window.showWarningMessage('Selecione uma organização primeiro.');
return;
}
const prompt = await vscode.window.showInputBox({
prompt: 'Descreva a tarefa em linguagem natural para o Qwen',
placeHolder: 'Ex: Corrigir lentidão no relatório PDF, categoria Bugfix e vincular ao projeto BeeTask',
ignoreFocusOut: true,
});
if (!prompt?.trim()) return;
try {
let parsed: any;
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Interpretando tarefa com IA (Qwen local)...',
cancellable: false,
},
async () => {
parsed = await api.parseTaskWithAi(orgId, prompt.trim());
}
);
if (!parsed || !parsed.title) {
vscode.window.showWarningMessage('Não foi possível interpretar os dados com a IA.');
return;
}
// Preview confirmation
const confirmMsg = `Criar tarefa: "${parsed.title}"?`;
const action = await vscode.window.showInformationMessage(
confirmMsg,
{ modal: false, detail: parsed.description || 'Sem descrição' },
'Criar Tarefa',
'Editar Título Antes',
'Cancelar'
);
if (action === 'Cancelar' || !action) return;
let finalTitle = parsed.title;
if (action === 'Editar Título Antes') {
const edited = await vscode.window.showInputBox({
prompt: 'Editar título',
value: parsed.title,
});
if (!edited?.trim()) return;
finalTitle = edited.trim();
}
const created = await api.createTask({
title: finalTitle,
description: parsed.description,
organizationId: orgId,
projectId: parsed.projectId,
categoryId: parsed.categoryId,
assigneeUserId: parsed.assigneeUserId,
externalUrl: parsed.externalUrl,
});
vscode.window.showInformationMessage(`✨ Tarefa #${created.id} criada via IA!`);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
const startNow = await vscode.window.showInformationMessage(
`Deseja iniciar o cronômetro para #${created.id}?`,
'Sim, Iniciar',
'Não'
);
if (startNow === 'Sim, Iniciar') {
await api.startTask(created.id);
await refreshAll(api, authManager, statusBar, treeProvider, idleDetector);
}
} catch (err: any) {
vscode.window.showErrorMessage(`Falha na criação com IA: ${err.message}`);
}
})
);
// 10. OPEN IN BROWSER
context.subscriptions.push(
vscode.commands.registerCommand('beetask.openInBrowser', async (arg?: Task | TaskTreeItem) => {
const config = vscode.workspace.getConfiguration('beetask');
const webUrl = config.get<string>('webUrl', 'https://task.beetech.dev').replace(/\/+$/, '');
let targetUrl = `${webUrl}/tasks`;
if (arg && 'task' in arg && arg.task?.id) {
targetUrl = `${webUrl}/tasks?taskId=${arg.task.id}`;
} else if (arg && 'id' in arg) {
targetUrl = `${webUrl}/tasks?taskId=${(arg as Task).id}`;
}
await vscode.env.openExternal(vscode.Uri.parse(targetUrl));
})
);
// 11. QUICK MENU (Status Bar Click)
context.subscriptions.push(
vscode.commands.registerCommand('beetask.quickMenu', async () => {
const isAuth = await authManager.isAuthenticated();
if (!isAuth) {
await vscode.commands.executeCommand('beetask.login');
return;
}
const activeTask = statusBar.getActiveTask();
const items: vscode.QuickPickItem[] = [];
if (activeTask) {
items.push({
label: `$(debug-pause) Pausar Tarefa Ativa (#${activeTask.id})`,
description: activeTask.title,
detail: 'Pausa o cronômetro e salva o tempo trabalhado',
});
items.push({
label: `$(check) Concluir Tarefa Ativa (#${activeTask.id})`,
description: activeTask.title,
detail: 'Marca a tarefa como COMPLETED e para o cronômetro',
});
items.push({
label: `$(link-external) Abrir Tarefa Ativa no Navegador`,
});
}
items.push({
label: `$(play) Iniciar Outra Tarefa...`,
detail: 'Escolha uma tarefa pendente para cronometrar',
});
items.push({
label: `$(plus) Criar Nova Tarefa`,
detail: 'Cria uma nova demanda no BeeTask',
});
items.push({
label: `$(sparkle) Criar Tarefa com IA`,
detail: 'Usa o Qwen local para estruturar uma demanda em linguagem natural',
});
items.push({
label: `$(organization) Trocar Organização (${authManager.getActiveOrgName() || 'Nenhuma'})`,
});
items.push({
label: `$(refresh) Atualizar Lista de Tarefas`,
});
items.push({
label: `$(sign-out) Desconectar Conta`,
});
const picked = await vscode.window.showQuickPick(items, {
placeHolder: activeTask
? `Tarefa em andamento: #${activeTask.id} ${activeTask.title}`
: 'BeeTask - Nenhuma tarefa em execução',
});
if (!picked) return;
if (picked.label.includes('Pausar Tarefa Ativa')) {
await vscode.commands.executeCommand('beetask.stopTask');
} else if (picked.label.includes('Concluir Tarefa Ativa')) {
await vscode.commands.executeCommand('beetask.completeTask');
} else if (picked.label.includes('Abrir Tarefa Ativa')) {
await vscode.commands.executeCommand('beetask.openInBrowser', activeTask);
} else if (picked.label.includes('Iniciar Outra Tarefa')) {
await vscode.commands.executeCommand('beetask.startTask');
} else if (picked.label.includes('Criar Nova Tarefa')) {
await vscode.commands.executeCommand('beetask.createTask');
} else if (picked.label.includes('Criar Tarefa com IA')) {
await vscode.commands.executeCommand('beetask.createTaskAi');
} else if (picked.label.includes('Trocar Organização')) {
await vscode.commands.executeCommand('beetask.selectOrg');
} else if (picked.label.includes('Atualizar')) {
await vscode.commands.executeCommand('beetask.refreshTasks');
} else if (picked.label.includes('Desconectar')) {
await vscode.commands.executeCommand('beetask.logout');
}
})
);
// 12. ITEM CLICK HANDLER
context.subscriptions.push(
vscode.commands.registerCommand('beetask.selectTaskItem', async (task: Task) => {
const isRunning = task.history && task.history.some(h => h.startAt && !h.endAt);
const items: vscode.QuickPickItem[] = [];
if (isRunning) {
items.push({
label: '$(debug-pause) Pausar Cronômetro',
detail: 'Para o tempo da tarefa',
});
} else if (task.status !== 'COMPLETED') {
items.push({
label: '$(play) Iniciar Cronômetro',
detail: 'Começa a cronometrar o tempo desta tarefa',
});
}
if (task.status !== 'COMPLETED') {
items.push({
label: '$(check) Marcar como Concluída',
});
}
items.push({
label: '$(link-external) Ver no Navegador',
});
const choice = await vscode.window.showQuickPick(items, {
placeHolder: `#${task.id} ${task.title}`,
});
if (!choice) return;
if (choice.label.includes('Iniciar')) {
await vscode.commands.executeCommand('beetask.startTask', task);
} else if (choice.label.includes('Pausar')) {
await vscode.commands.executeCommand('beetask.stopTask', task);
} else if (choice.label.includes('Concluída')) {
await vscode.commands.executeCommand('beetask.completeTask', task);
} else if (choice.label.includes('Ver no Navegador')) {
await vscode.commands.executeCommand('beetask.openInBrowser', task);
}
})
);
}
async function refreshAll(
api: BeeTaskApiClient,
authManager: AuthManager,
statusBar: StatusBarManager,
treeProvider: TaskTreeProvider,
idleDetector?: IdleDetector
): Promise<void> {
idleDetector?.recordActivity();
const isAuth = await authManager.isAuthenticated();
if (!isAuth) {
statusBar.setActiveTask(null);
treeProvider.refresh();
return;
}
treeProvider.refresh();
try {
const orgId = authManager.getActiveOrgId();
const tasks = await api.listMyTasks(orgId);
let activeTask: Task | null = null;
for (const t of tasks) {
if (t.history && t.history.some(h => h.startAt && !h.endAt)) {
activeTask = t;
break;
}
}
statusBar.setActiveTask(activeTask);
} catch (_) {
statusBar.setActiveTask(null);
}
}