feat(extension): criar extensao oficial do BeeTask para VS Code com timer e gestao de tarefas
This commit is contained in:
parent
f1782171ff
commit
e445ad565b
16 changed files with 5402 additions and 0 deletions
5
vscode-extension/.gitignore
vendored
Normal file
5
vscode-extension/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
*.vsix
|
||||
!beetask-vscode-1.0.0.vsix
|
||||
|
||||
7
vscode-extension/.vscodeignore
Normal file
7
vscode-extension/.vscodeignore
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
node_modules/**
|
||||
.gitignore
|
||||
tsconfig.json
|
||||
**/*.map
|
||||
21
vscode-extension/LICENSE.md
Normal file
21
vscode-extension/LICENSE.md
Normal 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.
|
||||
80
vscode-extension/README.md
Normal file
80
vscode-extension/README.md
Normal 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`).
|
||||
BIN
vscode-extension/beetask-vscode-1.0.0.vsix
Normal file
BIN
vscode-extension/beetask-vscode-1.0.0.vsix
Normal file
Binary file not shown.
1
vscode-extension/media/bee.svg
Normal file
1
vscode-extension/media/bee.svg
Normal file
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
3885
vscode-extension/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
172
vscode-extension/package.json
Normal file
172
vscode-extension/package.json
Normal 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
136
vscode-extension/src/api.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
83
vscode-extension/src/authManager.ts
Normal file
83
vscode-extension/src/authManager.ts
Normal 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 : '';
|
||||
}
|
||||
}
|
||||
554
vscode-extension/src/commands.ts
Normal file
554
vscode-extension/src/commands.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
65
vscode-extension/src/extension.ts
Normal file
65
vscode-extension/src/extension.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
107
vscode-extension/src/statusBarManager.ts
Normal file
107
vscode-extension/src/statusBarManager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
203
vscode-extension/src/taskTreeProvider.ts
Normal file
203
vscode-extension/src/taskTreeProvider.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
67
vscode-extension/src/types.ts
Normal file
67
vscode-extension/src/types.ts
Normal 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;
|
||||
}
|
||||
16
vscode-extension/tsconfig.json
Normal file
16
vscode-extension/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Reference in a new issue