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