feat(extension): criar extensao oficial do BeeTask para VS Code com timer e gestao de tarefas

This commit is contained in:
Gabriel Menezes 2026-11-05 11:06:31 -04:00
parent f1782171ff
commit e445ad565b
16 changed files with 5402 additions and 0 deletions

5
vscode-extension/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,7 @@
.vscode/**
.vscode-test/**
src/**
node_modules/**
.gitignore
tsconfig.json
**/*.map

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 BeeTech Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,80 @@
# BeeTask para VS Code 🐝⏱️
Extensão oficial do **BeeTask** para o Visual Studio Code. Gerencie suas demandas e rastreie o tempo trabalhado diretamente do seu editor de código, sem precisar alternar para o navegador.
---
## ✨ Recursos
- **⏱️ Cronômetro em Tempo Real:** Acompanhe o tempo trabalhado na tarefa ativa direto na Barra de Status (rodapé do VS Code).
- **📋 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.
- **✨ Criação com IA (Qwen Local):** Descreva a demanda em linguagem natural e o Qwen estrutura título, projeto e categoria automaticamente.
- **🌐 Acesso Direto:** Abra a tarefa no navegador web com um clique.
---
## 🚀 Como Instalar
### Opção 1: Instalação via arquivo `.vsix` (Recomendado)
1. No terminal, dentro da pasta `vscode-extension`, gere o pacote:
```bash
npm run package
```
*(Ou baixe o arquivo `beetask-vscode-1.0.0.vsix` pré-compilado).*
2. Instale no seu VS Code:
- **Pelo Terminal:**
```bash
code --install-extension beetask-vscode-1.0.0.vsix
```
- **Pela Interface:**
Abra o VS Code, vá na aba de Extensões (`Ctrl+Shift+X`), clique nos três pontinhos (`...`) no canto superior e selecione **"Install from VSIX..."** (*Instalar a partir de VSIX...*).
### Opção 2: Modo Desenvolvimento (Testar agora)
1. Abra a pasta `vscode-extension` no VS Code.
2. Execute `npm install`.
3. Pressione **`F5`** para iniciar a depuração em uma janela de testes.
---
## 🔑 Primeiro Acesso (Login)
1. Pressione `Ctrl+Shift+P` (ou `Cmd+Shift+P` no Mac) e digite:
```
BeeTask: Conectar Conta (Login)
```
2. Informe:
- **URL da API:** Padrão `https://api-task.beetech.dev`
- **Email:** Seu email de acesso
- **Senha:** Sua senha do BeeTask
3. Se você fizer parte de mais de uma organização, a extensão solicitará que escolha a organização ativa.
---
## ⌨️ Comandos Disponíveis (`Ctrl+Shift+P`)
| Comando | Descrição |
|---|---|
| `BeeTask: Iniciar Tarefa (Play)` | Seleciona e inicia o cronômetro de uma tarefa |
| `BeeTask: Pausar Tarefa (Pause)` | Pausa a contagem de tempo da tarefa ativa |
| `BeeTask: Concluir Tarefa` | Marca a tarefa como concluída |
| `BeeTask: Nova Tarefa` | Cria uma nova tarefa com título, projeto e categoria |
| `BeeTask: Criar Tarefa com IA` | Usa IA para interpretar texto livre e criar a tarefa |
| `BeeTask: Selecionar Organização` | Troca a organização de trabalho |
| `BeeTask: Atualizar Lista de Tarefas` | Recarrega as tarefas do servidor |
| `BeeTask: Abrir no Navegador` | Abre o BeeTask no seu navegador padrão |
| `BeeTask: Desconectar (Logout)` | Remove as credenciais salvas |
---
## ⚙️ Configurações
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`).

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

3885
vscode-extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,172 @@
{
"name": "beetask-vscode",
"displayName": "BeeTask",
"description": "Gerenciamento e rastreamento de tarefas com cronômetro integrado ao VS Code",
"version": "1.0.0",
"publisher": "beetech",
"repository": {
"type": "git",
"url": "https://github.com/beetechgroup-net/bee-task-v3.git"
},
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Productivity",
"Other"
],
"keywords": [
"tasks",
"timer",
"beetask",
"time-tracking",
"productivity"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "beetask.login",
"title": "BeeTask: Conectar Conta (Login)"
},
{
"command": "beetask.logout",
"title": "BeeTask: Desconectar (Logout)"
},
{
"command": "beetask.selectOrg",
"title": "BeeTask: Selecionar Organização"
},
{
"command": "beetask.refreshTasks",
"title": "BeeTask: Atualizar Lista de Tarefas",
"icon": "$(refresh)"
},
{
"command": "beetask.createTask",
"title": "BeeTask: Nova Tarefa",
"icon": "$(plus)"
},
{
"command": "beetask.createTaskAi",
"title": "BeeTask: Criar Tarefa com IA",
"icon": "$(sparkle)"
},
{
"command": "beetask.startTask",
"title": "Iniciar Tarefa (Play)",
"icon": "$(play)"
},
{
"command": "beetask.stopTask",
"title": "Pausar Tarefa (Pause)",
"icon": "$(debug-pause)"
},
{
"command": "beetask.completeTask",
"title": "Concluir Tarefa",
"icon": "$(check)"
},
{
"command": "beetask.openInBrowser",
"title": "Abrir no Navegador",
"icon": "$(link-external)"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "beetask-explorer",
"title": "BeeTask",
"icon": "media/bee.svg"
}
]
},
"views": {
"beetask-explorer": [
{
"id": "beetask.tasksView",
"name": "Minhas Tarefas"
}
]
},
"menus": {
"view/title": [
{
"command": "beetask.refreshTasks",
"when": "view == beetask.tasksView",
"group": "navigation@1"
},
{
"command": "beetask.createTask",
"when": "view == beetask.tasksView",
"group": "navigation@2"
},
{
"command": "beetask.createTaskAi",
"when": "view == beetask.tasksView",
"group": "navigation@3"
}
],
"view/item/context": [
{
"command": "beetask.startTask",
"when": "view == beetask.tasksView && viewItem == task-not-started",
"group": "inline@1"
},
{
"command": "beetask.startTask",
"when": "view == beetask.tasksView && viewItem == task-stopped",
"group": "inline@1"
},
{
"command": "beetask.stopTask",
"when": "view == beetask.tasksView && viewItem == task-running",
"group": "inline@1"
},
{
"command": "beetask.completeTask",
"when": "view == beetask.tasksView && viewItem =~ /^task-/",
"group": "inline@2"
},
{
"command": "beetask.openInBrowser",
"when": "view == beetask.tasksView && viewItem =~ /^task-/",
"group": "inline@3"
}
]
},
"configuration": {
"title": "BeeTask",
"properties": {
"beetask.serverUrl": {
"type": "string",
"default": "https://api-task.beetech.dev",
"description": "URL base da API do BeeTask"
},
"beetask.webUrl": {
"type": "string",
"default": "https://task.beetech.dev",
"description": "URL da interface web do BeeTask"
},
"beetask.refreshIntervalSeconds": {
"type": "number",
"default": 30,
"description": "Intervalo de atualização automática das tarefas (em segundos)"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"package": "vsce package --no-dependencies"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/vscode": "^1.85.0",
"@vscode/vsce": "^3.2.1",
"typescript": "^5.3.3"
}
}

136
vscode-extension/src/api.ts Normal file
View file

@ -0,0 +1,136 @@
import * as vscode from 'vscode';
import { AuthManager } from './authManager';
import { Task, Project, Category, LoginResponse, TaskStatus, ParsedAiTask } from './types';
export class BeeTaskApiClient {
private authManager: AuthManager;
constructor(authManager: AuthManager) {
this.authManager = authManager;
}
private getBaseUrl(): string {
const config = vscode.workspace.getConfiguration('beetask');
const url = config.get<string>('serverUrl', 'https://api-task.beetech.dev');
return url.replace(/\/+$/, '');
}
private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
const baseUrl = this.getBaseUrl();
const url = `${baseUrl}${path.startsWith('/') ? path : '/' + path}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Accept: 'application/json',
...((options.headers as Record<string, string>) || {}),
};
const jwt = await this.authManager.getJwt();
if (jwt) {
headers['Authorization'] = `Bearer ${jwt}`;
}
const response = await fetch(url, {
...options,
headers,
});
if (response.status === 401) {
await this.authManager.clearAuth();
vscode.window.showWarningMessage('Sessão do BeeTask expirada. Faça login novamente.');
throw new Error('Não autenticado.');
}
if (!response.ok) {
let errorText = '';
try {
const errJson = await response.json() as any;
errorText = errJson.message || errJson.error || JSON.stringify(errJson);
} catch (_) {
errorText = await response.text();
}
throw new Error(`Erro na API (${response.status}): ${errorText || response.statusText}`);
}
if (response.status === 204) {
return {} as T;
}
return (await response.json()) as T;
}
public async login(email: string, password: string): Promise<LoginResponse> {
const data = await this.request<LoginResponse>('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
await this.authManager.saveAuth(data);
return data;
}
public async listMyTasks(orgId?: number): Promise<Task[]> {
const query = orgId ? `?organizationId=${orgId}` : '';
return await this.request<Task[]>(`/tasks/mine${query}`, {
method: 'GET',
});
}
public async startTask(taskId: number): Promise<Task> {
return await this.request<Task>(`/tasks/${taskId}/start`, {
method: 'PUT',
});
}
public async stopTask(taskId: number): Promise<Task> {
return await this.request<Task>(`/tasks/${taskId}/stop`, {
method: 'PUT',
});
}
public async updateStatus(taskId: number, status: TaskStatus): Promise<Task> {
return await this.request<Task>(`/tasks/${taskId}/status`, {
method: 'PATCH',
body: JSON.stringify(status),
});
}
public async createTask(payload: {
title: string;
description?: string;
organizationId: number;
projectId?: number;
categoryId?: number;
}): Promise<Task> {
return await this.request<Task>('/tasks', {
method: 'POST',
body: JSON.stringify(payload),
});
}
public async parseTaskWithAi(organizationId: number, prompt: string): Promise<ParsedAiTask> {
return await this.request<ParsedAiTask>('/tasks/ai/parse', {
method: 'POST',
body: JSON.stringify({ organizationId, prompt }),
});
}
public async listProjects(orgId: number): Promise<Project[]> {
try {
return await this.request<Project[]>(`/organizations/${orgId}/projects`, {
method: 'GET',
});
} catch (_) {
return [];
}
}
public async listCategories(orgId: number): Promise<Category[]> {
try {
return await this.request<Category[]>(`/organizations/${orgId}/categories`, {
method: 'GET',
});
} catch (_) {
return [];
}
}
}

View file

@ -0,0 +1,83 @@
import * as vscode from 'vscode';
import { Organization, LoginResponse } from './types';
const SECRET_JWT_KEY = 'beetask.jwt';
const SECRET_REFRESH_KEY = 'beetask.refreshToken';
const STATE_USER_KEY = 'beetask.user';
const STATE_ORGS_KEY = 'beetask.organizations';
const STATE_ACTIVE_ORG_KEY = 'beetask.activeOrgId';
export class AuthManager {
private secrets: vscode.SecretStorage;
private state: vscode.Memento;
constructor(context: vscode.ExtensionContext) {
this.secrets = context.secrets;
this.state = context.globalState;
}
public async saveAuth(loginData: LoginResponse): Promise<void> {
await this.secrets.store(SECRET_JWT_KEY, loginData.jwt);
if (loginData.refreshToken) {
await this.secrets.store(SECRET_REFRESH_KEY, loginData.refreshToken);
}
await this.state.update(STATE_USER_KEY, {
name: loginData.name,
email: loginData.email,
photo: loginData.photo,
});
if (loginData.organizations && loginData.organizations.length > 0) {
await this.state.update(STATE_ORGS_KEY, loginData.organizations);
// If no active org selected yet, default to first
const currentOrgId = await this.getActiveOrgId();
if (!currentOrgId || !loginData.organizations.some(o => o.id === currentOrgId)) {
await this.setActiveOrgId(loginData.organizations[0].id);
}
}
}
public async getJwt(): Promise<string | undefined> {
return await this.secrets.get(SECRET_JWT_KEY);
}
public async getRefreshToken(): Promise<string | undefined> {
return await this.secrets.get(SECRET_REFRESH_KEY);
}
public getUser(): { name: string; email: string; photo?: string } | undefined {
return this.state.get(STATE_USER_KEY);
}
public getOrganizations(): Organization[] {
return this.state.get<Organization[]>(STATE_ORGS_KEY) || [];
}
public getActiveOrgId(): number | undefined {
return this.state.get<number>(STATE_ACTIVE_ORG_KEY);
}
public async setActiveOrgId(orgId: number): Promise<void> {
await this.state.update(STATE_ACTIVE_ORG_KEY, orgId);
}
public async clearAuth(): Promise<void> {
await this.secrets.delete(SECRET_JWT_KEY);
await this.secrets.delete(SECRET_REFRESH_KEY);
await this.state.update(STATE_USER_KEY, undefined);
await this.state.update(STATE_ORGS_KEY, undefined);
await this.state.update(STATE_ACTIVE_ORG_KEY, undefined);
}
public async isAuthenticated(): Promise<boolean> {
const jwt = await this.getJwt();
return !!jwt;
}
public getActiveOrgName(): string {
const activeId = this.getActiveOrgId();
if (!activeId) return '';
const orgs = this.getOrganizations();
const found = orgs.find(o => o.id === activeId);
return found ? found.name : '';
}
}

View file

@ -0,0 +1,554 @@
import * as vscode from 'vscode';
import { BeeTaskApiClient } from './api';
import { AuthManager } from './authManager';
import { StatusBarManager } from './statusBarManager';
import { TaskTreeProvider, TaskTreeItem } from './taskTreeProvider';
import { Task } from './types';
export function registerCommands(
context: vscode.ExtensionContext,
api: BeeTaskApiClient,
authManager: AuthManager,
statusBar: StatusBarManager,
treeProvider: TaskTreeProvider
): 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);
// 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);
}
})
);
// 4. REFRESH TASKS
context.subscriptions.push(
vscode.commands.registerCommand('beetask.refreshTasks', async () => {
await refreshAll(api, authManager, statusBar, treeProvider);
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);
} 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);
} 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);
} 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);
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);
}
} 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,
});
vscode.window.showInformationMessage(`✨ Tarefa #${created.id} criada via IA!`);
await refreshAll(api, authManager, statusBar, treeProvider);
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);
}
} 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
): Promise<void> {
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);
}
}

View file

@ -0,0 +1,65 @@
import * as vscode from 'vscode';
import { AuthManager } from './authManager';
import { BeeTaskApiClient } from './api';
import { StatusBarManager } from './statusBarManager';
import { TaskTreeProvider } from './taskTreeProvider';
import { registerCommands } from './commands';
let pollTimer: NodeJS.Timeout | null = null;
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const authManager = new AuthManager(context);
const api = new BeeTaskApiClient(authManager);
const statusBar = new StatusBarManager();
const treeProvider = new TaskTreeProvider(api, authManager);
// Register Tree View
const treeView = vscode.window.createTreeView('beetask.tasksView', {
treeDataProvider: treeProvider,
showCollapseAll: true,
});
context.subscriptions.push(treeView);
context.subscriptions.push(statusBar);
// Register Commands
registerCommands(context, api, authManager, statusBar, treeProvider);
// Initial Sync
if (await authManager.isAuthenticated()) {
await vscode.commands.executeCommand('beetask.refreshTasks');
}
// Periodic Auto-refresh
const config = vscode.workspace.getConfiguration('beetask');
const intervalSec = config.get<number>('refreshIntervalSeconds', 30);
if (intervalSec > 5) {
pollTimer = setInterval(async () => {
if (await authManager.isAuthenticated()) {
try {
const orgId = authManager.getActiveOrgId();
const tasks = await 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;
}
}
statusBar.setActiveTask(runningTask);
treeProvider.refresh();
} catch (_) {
// Silent polling error
}
}
}, intervalSec * 1000);
}
console.log('BeeTask VS Code extension activated successfully.');
}
export function deactivate(): void {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}

View file

@ -0,0 +1,107 @@
import * as vscode from 'vscode';
import { Task } from './types';
export class StatusBarManager {
private statusBarItem: vscode.StatusBarItem;
private activeTask: Task | null = null;
private timerInterval: NodeJS.Timeout | null = null;
private baseSeconds: number = 0;
private ongoingStartTime: number | null = null;
constructor() {
this.statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
100
);
this.statusBarItem.command = 'beetask.quickMenu';
this.updateIdle();
this.statusBarItem.show();
}
public setActiveTask(task: Task | null): void {
this.activeTask = task;
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
if (!task) {
this.updateIdle();
return;
}
// Calculate elapsed seconds from history
let finishedSec = 0;
let ongoingStart: string | null = null;
if (task.history && task.history.length > 0) {
for (const h of task.history) {
if (h.elapsedSeconds !== undefined && h.elapsedSeconds !== null) {
finishedSec += h.elapsedSeconds;
} else if (h.startAt && !h.endAt) {
ongoingStart = h.startAt;
}
}
}
this.baseSeconds = finishedSec;
if (ongoingStart) {
this.ongoingStartTime = new Date(ongoingStart).getTime();
} else {
this.ongoingStartTime = Date.now();
}
this.renderActiveTick();
this.timerInterval = setInterval(() => {
this.renderActiveTick();
}, 1000);
}
public getActiveTask(): Task | null {
return this.activeTask;
}
private updateIdle(): void {
this.statusBarItem.text = '$(clock) BeeTask';
this.statusBarItem.tooltip = 'BeeTask: Nenhuma tarefa em execução. Clique para iniciar.';
this.statusBarItem.backgroundColor = undefined;
}
private renderActiveTick(): void {
if (!this.activeTask) return;
let currentSeconds = this.baseSeconds;
if (this.ongoingStartTime) {
const live = Math.max(0, Math.floor((Date.now() - this.ongoingStartTime) / 1000));
currentSeconds += live;
}
const timeFormatted = this.formatSeconds(currentSeconds);
const shortTitle = this.activeTask.title.length > 25
? this.activeTask.title.substring(0, 22) + '...'
: this.activeTask.title;
this.statusBarItem.text = `$(play) [BeeTask] #${this.activeTask.id} ${shortTitle} (${timeFormatted})`;
this.statusBarItem.tooltip = `Tarefa em execução: #${this.activeTask.id} - ${this.activeTask.title}\nTempo total: ${timeFormatted}\nClique para ver opções.`;
this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
}
private formatSeconds(totalSec: number): string {
const hours = Math.floor(totalSec / 3600);
const minutes = Math.floor((totalSec % 3600) / 60);
const seconds = totalSec % 60;
const pad = (n: number) => n.toString().padStart(2, '0');
if (hours > 0) {
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}
return `${pad(minutes)}:${pad(seconds)}`;
}
public dispose(): void {
if (this.timerInterval) {
clearInterval(this.timerInterval);
}
this.statusBarItem.dispose();
}
}

View file

@ -0,0 +1,203 @@
import * as vscode from 'vscode';
import { Task } from './types';
import { BeeTaskApiClient } from './api';
import { AuthManager } from './authManager';
export type TreeItemType = 'category' | 'task';
export class TaskTreeItem extends vscode.TreeItem {
public task?: Task;
constructor(
label: string,
collapsibleState: vscode.TreeItemCollapsibleState,
task?: Task,
contextValue?: string
) {
super(label, collapsibleState);
this.task = task;
if (contextValue) {
this.contextValue = contextValue;
}
}
}
export class TaskTreeProvider implements vscode.TreeDataProvider<TaskTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<TaskTreeItem | undefined | null | void> =
new vscode.EventEmitter<TaskTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<TaskTreeItem | undefined | null | void> =
this._onDidChangeTreeData.event;
private tasks: Task[] = [];
private activeTaskId: number | null = null;
private api: BeeTaskApiClient;
private authManager: AuthManager;
constructor(api: BeeTaskApiClient, authManager: AuthManager) {
this.api = api;
this.authManager = authManager;
}
public refresh(): void {
this._onDidChangeTreeData.fire();
}
public getTreeItem(element: TaskTreeItem): vscode.TreeItem {
return element;
}
public async getChildren(element?: TaskTreeItem): Promise<TaskTreeItem[]> {
const isAuth = await this.authManager.isAuthenticated();
if (!isAuth) {
const loginItem = new TaskTreeItem(
'Clique aqui para conectar sua conta BeeTask...',
vscode.TreeItemCollapsibleState.None
);
loginItem.iconPath = new vscode.ThemeIcon('sign-in');
loginItem.command = {
command: 'beetask.login',
title: 'Conectar Conta',
};
return [loginItem];
}
if (!element) {
// Root level - Fetch tasks
try {
const orgId = this.authManager.getActiveOrgId();
this.tasks = await this.api.listMyTasks(orgId);
} catch (err: any) {
const errorItem = new TaskTreeItem(
`Erro ao carregar: ${err.message}`,
vscode.TreeItemCollapsibleState.None
);
errorItem.iconPath = new vscode.ThemeIcon('error');
return [errorItem];
}
// Check which task is running (has ongoing history without endAt)
this.activeTaskId = null;
for (const t of this.tasks) {
if (t.history && t.history.some(h => h.startAt && !h.endAt)) {
this.activeTaskId = t.id;
break;
}
}
const items: TaskTreeItem[] = [];
// Section 1: Running
const runningTasks = this.tasks.filter(t => t.id === this.activeTaskId);
if (runningTasks.length > 0) {
const secRunning = new TaskTreeItem(
`⏱️ EM EXECUÇÃO (${runningTasks.length})`,
vscode.TreeItemCollapsibleState.Expanded
);
items.push(secRunning);
}
// Section 2: Pending / In Progress
const pendingTasks = this.tasks.filter(
t => t.id !== this.activeTaskId && t.status !== 'COMPLETED' && t.status !== 'CANCELED'
);
const secPending = new TaskTreeItem(
`📋 A FAZER (${pendingTasks.length})`,
vscode.TreeItemCollapsibleState.Expanded
);
items.push(secPending);
// Section 3: Completed
const completedTasks = this.tasks.filter(t => t.status === 'COMPLETED');
if (completedTasks.length > 0) {
const secCompleted = new TaskTreeItem(
`✅ CONCLUÍDAS (${completedTasks.length})`,
vscode.TreeItemCollapsibleState.Collapsed
);
items.push(secCompleted);
}
return items;
}
// Children of sections
if (element.label?.toString().startsWith('⏱️ EM EXECUÇÃO')) {
const runningTasks = this.tasks.filter(t => t.id === this.activeTaskId);
return runningTasks.map(t => this.createTaskItem(t, true));
}
if (element.label?.toString().startsWith('📋 A FAZER')) {
const pendingTasks = this.tasks.filter(
t => t.id !== this.activeTaskId && t.status !== 'COMPLETED' && t.status !== 'CANCELED'
);
if (pendingTasks.length === 0) {
const emptyItem = new TaskTreeItem('Nenhuma tarefa pendente', vscode.TreeItemCollapsibleState.None);
emptyItem.iconPath = new vscode.ThemeIcon('check');
return [emptyItem];
}
return pendingTasks.map(t => this.createTaskItem(t, false));
}
if (element.label?.toString().startsWith('✅ CONCLUÍDAS')) {
const completedTasks = this.tasks.filter(t => t.status === 'COMPLETED');
return completedTasks.map(t => this.createTaskItem(t, false));
}
return [];
}
private createTaskItem(task: Task, isRunning: boolean): TaskTreeItem {
const item = new TaskTreeItem(
`#${task.id} ${task.title}`,
vscode.TreeItemCollapsibleState.None,
task
);
const parts: string[] = [];
if (task.project?.name) parts.push(task.project.name);
if (task.category?.name) parts.push(task.category.name);
item.description = parts.join(' • ');
if (isRunning) {
item.iconPath = new vscode.ThemeIcon('play', new vscode.ThemeColor('charts.green'));
item.contextValue = 'task-running';
} else if (task.status === 'COMPLETED') {
item.iconPath = new vscode.ThemeIcon('pass', new vscode.ThemeColor('charts.green'));
item.contextValue = 'task-completed';
} else if (task.status === 'IN_PROGRESS') {
item.iconPath = new vscode.ThemeIcon('play-circle', new vscode.ThemeColor('charts.yellow'));
item.contextValue = 'task-stopped';
} else {
item.iconPath = new vscode.ThemeIcon('circle-outline', new vscode.ThemeColor('charts.blue'));
item.contextValue = 'task-not-started';
}
// Tooltip
const tooltip = new vscode.MarkdownString();
tooltip.appendMarkdown(`**#${task.id} ${task.title}**\n\n`);
if (task.description) {
tooltip.appendMarkdown(`${task.description}\n\n`);
}
tooltip.appendMarkdown(`- **Status:** ${task.status}\n`);
if (task.project?.name) tooltip.appendMarkdown(`- **Projeto:** ${task.project.name}\n`);
if (task.category?.name) tooltip.appendMarkdown(`- **Categoria:** ${task.category.name}\n`);
item.tooltip = tooltip;
// Command when item is clicked: show quick options
item.command = {
command: 'beetask.selectTaskItem',
title: 'Opções da Tarefa',
arguments: [task],
};
return item;
}
public getTasks(): Task[] {
return this.tasks;
}
public getActiveTask(): Task | null {
if (!this.activeTaskId) return null;
return this.tasks.find(t => t.id === this.activeTaskId) || null;
}
}

View file

@ -0,0 +1,67 @@
export type TaskStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELED';
export interface TaskHistoryItem {
id?: number;
startAt: string;
endAt?: string | null;
elapsedSeconds?: number | null;
}
export interface Project {
id: number;
name: string;
}
export interface Category {
id: number;
name: string;
color?: string;
icon?: string;
}
export interface TaskUser {
id: number;
name: string;
email: string;
photo?: string;
}
export interface Task {
id: number;
title: string;
description?: string;
status: TaskStatus;
project?: Project;
category?: Category;
user?: TaskUser;
createdBy?: TaskUser;
finishedAt?: string;
history?: TaskHistoryItem[];
externalUrl?: string;
}
export interface Organization {
id: number;
name: string;
roles?: string[];
}
export interface LoginResponse {
name: string;
email: string;
photo?: string;
jwt: string;
refreshToken?: string;
expiresIn?: number;
issuedAt?: string;
organizations?: Organization[];
}
export interface ParsedAiTask {
title: string;
description: string;
projectId?: number;
categoryId?: number;
assigneeUserId?: number;
status?: TaskStatus;
}

View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"outDir": "dist",
"lib": ["ES2022"],
"sourceMap": true,
"rootDir": "src",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src"]
}