vscode-to-desktop · Deskhop

Manda janelas do Visual Studio Code pra virtual desktops do Windows 11 por linguagem natural — cada projeto no seu próprio desktop, sem mouse e sem Task View.
Status: ✅ Funcional (v0.7.2) — provado ponta-a-ponta no Windows 11 25H2 (build 26200). Publicado como extensão do VSCode (lucasftas.deskhop).
Extensão Deskhop (jeito mais fácil — botões nativos)
Além do script, o projeto é publicado como a extensão Deskhop no marketplace: instala e já traz os 3 botões na barra de status (Desktop novo · Espalhar · Juntar), sem precisar configurar nada.
code --install-extension lucasftas.deskhop
O clique é silencioso: nada de painel abrindo. A extensão roda o vscode-desktops.ps1 (embutido no pacote) por child_process com a janela do console escondida, mostrando só um aviso discreto na barra de status enquanto executa — erro, e só erro, vira notificação com "Ver log". O -Foreground continua funcionando porque nada rouba o foco.
📋 Toda execução vai pro log. Silencioso não quer dizer sem rastro: o canal Deskhop no painel de Saída registra horário, linha de comando, saída do script e exit code de cada clique — inclusive dos que deram certo. Abre pela paleta em Deskhop: Ver log. Falha parcial (uma janela que não moveu) faz o script sair com exit code ≠ 0, então ela vira notificação em vez de sumir.
| Setting |
Padrão |
O que faz |
deskhop.maximizeOnSpread |
true |
Ao Espalhar, cada janela chega em tela cheia no monitor principal. Desligue pra manter o tamanho/monitor de cada uma (spread -NoMaximize). |
deskhop.closeDesktopsOnGather |
true |
Ao Juntar, fecha os virtual desktops. Desligue pra só trazer as janelas do VSCode, mantendo os desktops (gather -KeepDesktops). |
deskhop.gatherUnifiesEverything |
true |
O Juntar traz também as janelas de outros programas e some com todos os desktops — fica um só. Desligue pra mexer apenas no VSCode, preservando desktop que tem outro programa (gather -VscodeOnly). |
deskhop.showTerminal |
false |
Mostra o comando rodando no terminal integrado (abre o painel de baixo). Só pra depurar. |
deskhop.powershellPath |
"" |
Caminho do PowerShell. Vazio = tenta o pwsh (PowerShell 7) e, se não existir, o powershell.exe que já vem no Windows. Preencha só se o seu estiver fora do PATH. |
Como é uma extensão do marketplace, o Settings Sync a replica em todas as máquinas logadas na sua conta. Só Windows 11 (fora disso os botões não aparecem). Detalhe da extensão em docs/BOTAO-STATUSBAR.md.
Proposta / Visão
Quem trabalha com vários workspaces do VSCode abertos ao mesmo tempo (um por projeto) acaba com todas as janelas empilhadas num desktop só. O Windows 11 tem virtual desktops (Win+Tab), mas:
- não tem atalho de teclado nativo pra mover uma janela já aberta pra outro desktop (só arrastando no Task View);
- e mesmo via script, enumerar as janelas do VSCode é traiçoeiro — todas pertencem a um único processo
Code.exe (Electron), então a abordagem óbvia (Get-Process Code) enxerga uma janela só.
Esta ferramenta resolve os dois: um script PowerShell que enumera todas as janelas do VSCode (via Win32 EnumWindows) e move cada uma pro virtual desktop que você quiser, criando/nomeando desktops na hora.
Problema que resolve
| Dor |
Antes |
Com a ferramenta |
| Mover janela aberta entre desktops |
Só arrastando no Task View (mouse) |
send, um comando |
| Organizar N projetos |
Um a um, na mão |
spread — cada projeto no seu desktop de uma vez |
| Achar as janelas do VSCode |
Get-Process vê só 1 |
EnumWindows pega todas |
| Voltar ao normal |
Fechar desktops um a um |
gather — junta tudo e sobra um desktop só |
Usuário-alvo
Power-users com muitos workspaces do VSCode abertos que querem separar contexto visual por projeto — sem instalar tiling window manager nem trocar o fluxo de trabalho.
Escopo do MVP (v0.1.0)
- Listar desktops + em qual desktop está cada janela do VSCode (
list)
- Mover uma janela específica (por nome de workspace ou HWND) pra um desktop, criando-o se não existir (
send)
- Mover a janela em foco pra um desktop novo, seguindo a janela (
send sem alvo, ou -Foreground)
- Espalhar todas as janelas do VSCode, uma por desktop (
spread)
- Juntar tudo de volta no desktop atual (
gather)
- Criar/nomear desktop (
new)
Fora de escopo (v0.1.0)
- Persistência entre reboots (Windows devolve tudo pro Desktop 1 ao reiniciar)
- Vigia residente / atalho de teclado global / tiling WM (decisão de projeto: operar sob demanda, não residente)
- Outros apps além do VSCode (a lógica generaliza, mas o foco é
Code.exe)
Premissa crítica (se falsa, mata o MVP)
A COM interna IVirtualDesktopManagerInternal do shell é acessível no Windows 11 25H2 (26200) e permite criar desktop + mover janela de outro processo.
Status: CONFIRMADA (a API pública IVirtualDesktopManager NÃO faz isso — dá E_ACCESSDENIED). O 25H2 é enablement package sobre o 24H2 (26100): mesmos binários, mesmo IID de COM. Provado empírico na máquina de referência (bind + move + verify E2E). Ver docs/PESQUISA.md.
Stack e arquitetura
- PowerShell (Windows PowerShell 5.1 / PowerShell 7) — sem admin
- Módulo
VirtualDesktop (MScholtes, PSGallery) v1.5.11 — fala com a COM interna do shell, auto-detecta o build/IID
- Win32
EnumWindows via Add-Type (C# inline, compilado pelo csc nativo — não instala nada) — resolve o gotcha Electron
- Delegação por-tarefa a modelo Haiku (quando operado via Claude Code) — a demanda é trivial, roda barato
Como usar
O script auto-instala o módulo VirtualDesktop na 1ª execução (sem admin, e sem perguntar nada — a extensão roda o script escondido, então um prompt travaria o clique). Se a instalação automática falhar, o erro diz o comando exato pra rodar uma vez num terminal visível:
Install-Module VirtualDesktop -Scope CurrentUser
⚠️ O módulo é instalado por interpretador: tê-lo no PowerShell 7 não o coloca no powershell.exe 5.1, e vice-versa. Se a extensão cair no fallback (máquina sem PowerShell 7), pode ser preciso instalar de novo, lá.
Comandos (rodar sem PowerShell elevado):
# Ver desktops + onde está cada janela do VSCode
pwsh -File scripts\vscode-desktops.ps1 list
# Mandar o workspace "facialscale" pra um desktop novo chamado "Facial"
pwsh -File scripts\vscode-desktops.ps1 send -Match facialscale -To Facial
# Espalhar TODAS as janelas, cada uma no seu desktop (nomeado pelo workspace)
# Cada janela já chega em tela cheia (maximizada) no monitor PRINCIPAL do Windows
pwsh -File scripts\vscode-desktops.ps1 spread
# Espalhar mantendo o tamanho/monitor de cada janela
pwsh -File scripts\vscode-desktops.ps1 spread -NoMaximize
# Juntar todas de volta no desktop atual E fechar os desktops de onde elas saíram
pwsh -File scripts\vscode-desktops.ps1 gather
# Ver o que aconteceria, sem mover nem fechar nada
pwsh -File scripts\vscode-desktops.ps1 spread -DryRun
pwsh -File scripts\vscode-desktops.ps1 gather -DryRun
# Só criar/nomear um desktop
pwsh -File scripts\vscode-desktops.ps1 new -To Comercial
# Mover a janela EM FOCO ("move essa janela"): cria desktop nomeado pelo workspace e segue.
# Rodar do terminal integrado DA PRÓPRIA janela VSCode que quer mover (ela fica em foco).
pwsh -File scripts\vscode-desktops.ps1 send -Foreground
Switches: -Hwnd <n> (mira por handle exato) · -To <idx|nome> (destino) · -FollowWindow (send/move segue a janela) · -NoMaximize (spread não maximiza) · -Maximize (send/move maximiza no monitor principal) · -KeepDesktops (gather não fecha desktop) · -VscodeOnly (gather fecha só os desktops de onde saiu VSCode) · -DryRun (spread/gather só mostram o que fariam). move é alias de send. Passar o switch de um verbo pra outro (spread -VscodeOnly) erra na cara em vez de ser ignorado em silêncio.
🪟 O spread espalha TODAS as janelas do VSCode — inclusive a que está sem pasta aberta (aparece como Visual Studio Code) e as isoladas. Cada uma ganha o desktop dela; nenhuma fica empilhada em cima de outra. Quando duas janelas rendem o mesmo nome (duas sem pasta, ou o mesmo workspace aberto duas vezes), a segunda vira nome (2), a terceira nome (3). O spread não fecha desktop vazio — quem consolida é o gather.
🧹 O gather unifica: sobra um desktop só. Ele traz pro desktop atual todas as janelas — as do VSCode e as dos outros programas — e fecha os demais virtual desktops. Nenhuma janela é fechada: fechar um virtual desktop no Windows apenas realoca as janelas dele (testado), e o gather ainda move tudo explicitamente antes, pra o destino ser o seu desktop e não depender da ordem de remoção. Isso resolve o caso chato do desktop que parece vazio mas tem uma janela minimizada dentro. Use -DryRun pra ver a decisão antes, -VscodeOnly pra mexer só no VSCode (preservando desktop de outros programas) e -KeepDesktops pra só juntar as janelas.
🖥️ Tela cheia no monitor principal: o spread maximiza cada janela no monitor principal do Windows (o que tem a origem 0,0) depois de mandá-la pro desktop dela — assim cada desktop novo já abre com a janela cheia, em vez de herdar o tamanho/monitor de antes. Feito via SetWindowPlacement (não ativa nem rouba o foco, e funciona com a janela já em outro virtual desktop). Desliga com -NoMaximize.
⚠️ Sobre -Foreground: pega a janela em foco no instante da execução (GetForegroundWindow). Funciona quando a janela-alvo está em foco. Disparado por automação sem foco ativo numa janela do VSCode, ele não move nada (falha segura) e pede -Match/-Hwnd.
Botão na barra de status (opcional — sem digitar comando)
Pra quem prefere clicar a digitar, dá pra ter os verbos como botões na barra de status do VSCode, via a extensão VsCode Action Buttons — sem escrever extensão própria.
O clique roda o comando no terminal integrado da própria janela → a janela do VSCode fica em foco, então o -Foreground funciona (mesmo motivo pelo qual o terminal integrado funciona e a automação desanexada não).
code --install-extension seunlanlege.action-buttons
Depois, no settings.json (global ou do workspace), 3 botões com ícones de linha nativos (codicons):
"actionButtons": {
"reloadButton": null,
"commands": [
{ "name": "$(empty-window) Desktop novo", "singleInstance": true, "focus": false,
"command": "pwsh -NoProfile -File 'CAMINHO/vscode-desktops.ps1' send -Foreground" },
{ "name": "$(multiple-windows) Espalhar", "singleInstance": true, "focus": false,
"command": "pwsh -NoProfile -File 'CAMINHO/vscode-desktops.ps1' spread" },
{ "name": "$(combine) Juntar", "singleInstance": true, "focus": false,
"command": "pwsh -NoProfile -File 'CAMINHO/vscode-desktops.ps1' gather" }
]
}
Ctrl+Shift+P → Refresh Action Buttons (ou reabre a janela) e os botões aparecem no canto inferior esquerdo. Receita completa e explicação em docs/BOTAO-STATUSBAR.md.
Estrutura de pastas
vscode-to-desktop/
├── README.md # este arquivo (estado funcional)
├── CHANGELOG.md # histórico de versões (Keep a Changelog)
├── CONTRIBUTING.md # como rodar, testar e as armadilhas do projeto
├── SECURITY.md # como reportar falha + superfície de risco
├── CODE_OF_CONDUCT.md # Contributor Covenant 2.1
├── LICENSE # MIT
├── CLAUDE.md # regras do projeto pra o Claude Code
├── .github/
│ ├── workflows/ # CI (lint + build + testes + VSIX) e Release por tag
│ ├── ISSUE_TEMPLATE/ # bug / ideia
│ └── PULL_REQUEST_TEMPLATE.md
├── assets/
│ └── icon.png # ícone da extensão (128×128)
├── docs/
│ ├── PESQUISA.md # pesquisa profunda + verificação adversarial (o "porquê" técnico)
│ ├── AUDITORIA-RISCOS.md # levantamento do que pode quebrar (e o que a verificação derrubou)
│ ├── CHECKLIST-MANUAL.md # cenários que nenhum teste automatizado cobre
│ ├── BOTAO-STATUSBAR.md # botão na barra de status via Action Buttons (alternativa)
│ └── specs/
│ ├── PRD.md # decisões técnicas + fontes + snippets
│ └── Spec.md # milestones (o que foi construído + critério de pronto)
├── scripts/
│ └── vscode-desktops.ps1 # a ferramenta
├── src/
│ ├── extension.ts # extensão: registra comandos e botões da barra de status
│ └── verbs.ts # lógica pura (botões + verbo/linha de comando), testável sem VSCode
└── tests/
├── verbs.test.mjs # testes da extensão (node --test)
└── vscode-desktops.Tests.ps1 # testes do script (Pester 5) — não movem janela
Os arquivos IMPLEMENTATIONS.md, OPERATIONS.md e SESSIONS.md na raiz são o registro interno de sessões e operações.
Desenvolvimento
npm install
npm run compile # tsc
npm run lint # eslint
npm test # compile + testes da extensão + testes Pester do script
npm run package # gera dist/deskhop-<versão>.vsix
F5 no VSCode abre uma janela de desenvolvimento com a extensão carregada.
O CI (.github/workflows/ci.yml) roda em windows-latest porque a ferramenta é Windows-only. Movimento de janela não é coberto por teste automatizado de propósito — mexeria na tela de quem roda; a validação é manual, com janela descartável (ver CONTRIBUTING.md).
Riscos conhecidos
- IID da COM interna muda a cada feature update grande do Windows (22H2→23H2→24H2). O 25H2 está salvo (enablement do 24H2); a quebra virá num update que reescreva o shell. Mitigação:
Update-Module VirtualDesktop. Fallback sempre disponível: Task View nativo.
- O alternador de desktops do shell pode crashar sob rajada de operações (classe MScholtes/VirtualDesktop#106 — aconteceu de verdade num
spread: access violation em Switcher.dll, o explorer.exe reinicia e a conexão COM do processo morre). Desde a 0.8.0 o script re-tenta o engasgo transiente com backoff, espaça as operações e, se o explorer reiniciou, sinaliza exit 3 — a extensão repete a operação uma vez num processo novo, que completa o que faltou (desktops e janelas já movidas sobrevivem ao restart).
- ⛔ Não rodar elevado — um processo em high integrity não move janela de app medium (o shell roda medium) → falha silenciosa.
- Mover não quebra o VSCode (confirmado — bug
microsoft/vscode#146915: funciona normal após mover; o único problema é não lembrar a posição após restart).
Releases
Versão atual: v0.8.0. Todas as releases (com o .vsix anexado) em:
https://github.com/lucasftas/vscode-to-desktop/releases
Licença
MIT © Lucas Freitas.