Skip to content

fix(terminal): clique sem seleção chega ao programa do pane (GLPI #272) - #24

Merged
gianlukamoraes merged 3 commits into
mainfrom
fix/glpi-272
Oct 28, 2026
Merged

gianlukamoraes merged 3 commits into
mainfrom
fix/glpi-272

Conversation

@gianlukamoraes

Copy link
Copy Markdown

Chamado GLPI devswha#272 (Cláudio Carrijo, DevDA 0.9.3 / UI f8bb39e): "Não consigo fechar a tela do lado direito."

Faz parte dos glpi-pequenos, pedido do Gianluka em 26/10/2026.

O que é a tela: o painel de diff do próprio Claude Code ("No changes this session"). No modo fullscreen ele abre sozinho com 144 colunas ou mais e fecha pelo ✕ no canto ou por /diff.

Causa: com o mouse tracking ligado, PaneTerminal.tsx forçava Shift em todo mousedown esquerdo para o arrasto selecionar. Com isso nenhum clique chegava ao pty, e o ✕ (como qualquer controle de mouse de um programa no pane) não respondia. O comentário dizia que herdr terminal attach ignora clique esquerdo, mas não ignora: num herdr terminal attach no tmux, um clique SGR no ✕ fechou o painel.

Mudança

  • onMouseUp: um clique que não selecionou nada (mesma célula, sem rolagem) é reenviado ao xterm como mousedown/mouseup sem o modificador. O xterm codifica o clique no modo que o pane pediu (SGR no herdr).
  • onMouseDown ignora evento não confiável, então o clique reenviado não ganha Shift.
  • O arrasto continua selecionando e copiando como antes. O duplo clique ainda seleciona a palavra (o 1º clique chega ao pane).

Testes

  • bun run typecheck e bun run build: ok
  • bun run test:unit: 2200 testes, 0 falhas
  • bun run check run bun scripts/ui-regression.ts (Chrome real, herdr isolado): 89 PASS, incluindo o caso novo "a terminal click reaches the pane and a drag sends no click". Sem o fix, o caso novo falha com Timed out: click as a mouse report.

Paliativo até o deploy: digitar /diff no prompt do Claude Code fecha o painel, e ele não volta a abrir sozinho.

🤖 Generated with Claude Code

gianlukamoraes and others added 2 commits October 27, 2026 08:43
…swha#272)

Com o mouse tracking ligado, todo clique esquerdo ganhava Shift forçado
e virava seleção, então nenhum clique chegava ao pty. O ✕ que fecha o
painel de diff do Claude Code não respondia. O herdr repassa o clique
(testado com herdr terminal attach): um clique que não selecionou nada
é reenviado ao xterm sem o modificador, e um arrasto continua
selecionando e copiando.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gianlukamoraes gianlukamoraes left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 APROVADO — clique simples volta a chegar ao programa do pane (GLPI devswha#272)

🧑 Resumo (para humanos)

  • 🟢 Corrige o chamado: um clique que não seleciona nada agora chega ao programa do terminal, então o ✕ do painel de diff do Claude Code volta a funcionar. Arrastar continua selecionando e copiando como antes.
  • 🟡 Clique com a mão tremendo — se o ponteiro passar para a célula vizinha entre apertar e soltar, vira seleção de 1–2 letras e o clique não chega ao programa. É raro e o usuário tenta de novo. (não bloqueia)
  • 🟡 Gate automático sem configuração para este repo — o monitor não tem a matriz de testes do herdr-web-ui, então rodei os testes à mão (abaixo). (não bloqueia; falta rodar /pr-monitor:setup no repo)

O que fazer: nada obrigatório. Os critérios de merge estão atendidos; este monitor está em modo revisor e não faz merge.

🤖 Detalhe técnico (file:line, fix, cenário) — para o Claude Code / quem for corrigir

[N-1] Limiar de "clique" é a mesma célula — src/components/PaneTerminal.tsx:761

  • Cenário: mousedown na col 10 e mouseup na col 11 (trackpad/jitter). d.anchor !== d.cursor, e o mousedown com Shift forçado fez o xterm criar uma seleção de 2 células → hasSelection() true → copia "ab" e não envia o clique.
  • Fix (opcional): tratar como clique se o deslocamento em pixels for < ~4 px (guardar clientX/Y no Drag) e chamar term.clearSelection() antes de reenviar. Só vale a pena se aparecer reclamação.

[N-2] Gate — pr-gate.sh 24 → rc=2 GATE SETUP FAILED: sem matriz para dataagile/herdr-web-ui. Isso não é verdict. Rodei à mão num worktree no head 95e75c9:

  • bun run typecheck → ok
  • bun run build → ok
  • bun run test:unit → 2194 pass, 6 skip, 0 fail (2200 testes)
  • scripts/ui-regression.ts (Chrome real) não foi rodado aqui. Vale o resultado do autor no corpo do PR (89 PASS, o caso novo falha sem o fix).

Verificado limpo:

  • Sem recursão nem drag fantasma: o mousedown reenviado tem isTrusted=false e onMouseDown (capture no host) retorna antes de forçar Shift ou criar drag. O mouseup reenviado chega a onMouseUp com drag === null (zerado antes do dispatch) e retorna.
  • O xterm reporta o evento sintético: com tracking ligado, SelectionService.handleMouseDown retorna cedo (!_enabled && !shouldForceSelection) e o handler de mouse do xterm codifica press/release no modo pedido (SGR). O teste de UI cobre isso: um press e um release na mesma célula, e um arrasto sem report.
  • O clique depois de uma seleção não estende a seleção: _handleIncrementalClick só roda com _enabled, que é false em modo mouse. Com Shift forçado, o clique cai em _handleSingleClick, a seleção some e o clique é reenviado ao pane.
  • Modo observar / prompt de senha / held: o report sai por term.onData, que já descarta input nesses estados (PaneTerminal.tsx:1044). O replay não fura o modo observar.
  • Touch e margem: mousedown vindo de toque e clique fora de term.element (margem do grid seguido) não criam drag, então não há replay. O comportamento anterior continua igual.
  • Duplo clique: o 1º clique chega ao pane. O 2º (detail=2, com Shift forçado) seleciona a palavra e copia, como o PR descreve.
  • CHANGELOG em ### Fixed com link do PR. Nenhum arquivo fora do escopo.

Checks de UI: não há campo de texto editável alterado (o check de digitação não se aplica). Não há regra de validação nova. A evidência é o teste em Chrome real em scripts/ui-regression.ts, que falha sem o fix, mais a reprodução manual via herdr terminal attach descrita no corpo.

@gianlukamoraes gianlukamoraes added the needs-owner Reviewed by pr-monitor; waiting for a human to decide or merge label Oct 27, 2026
@gianlukamoraes

Copy link
Copy Markdown
Author

Revisão do #24 (head 95e75c9)

Veredito: precisa de ajustes. O caminho principal funciona: um clique sem seleção chega ao pane como SGR, e o arrasto continua só selecionando. Mas o clique reenviado abre o link duas vezes. A correção é de uma linha.

Achados confirmados

1. Clique num link abre o link duas vezes (src/components/PaneTerminal.tsx:763-765)
O clique reenviado é disparado em event.target, que fica dentro de .xterm-screen. O Linkifier do xterm escuta mousedown/mouseup justamente no screen (node_modules/@xterm/xterm/src/browser/Linkifier.ts:55-56). Ele ativa o link sempre que o press e o release caem nele (:229-230) e não olha isTrusted. O linkPressed (PaneTerminal.tsx:364) só confere button === 0 && !hasSelection(), e o par sintético passa nas duas condições. O handler do WebLinksAddon (:394) nem chama o linkPressed.
Cenário: num pane com mouse tracking ligado (Claude Code, por exemplo), clicar numa URL. O clique real abre a URL, o clique reenviado abre de novo: duas abas, ou a segunda barrada pelo bloqueador de pop-up. Num link de arquivo, o openFile roda duas vezes.
Reproduzi numa página isolada com xterm 5.5.0, WebLinksAddon e a mesma lógica de onMouseDown/onMouseUp do PR, rodando no Chromium: com o fix, 2 aberturas; sem o fix, 1.
Sugestão: disparar o par em term.element em vez de event.target. O bindMouse (Terminal.ts:774) e o SelectionService (Terminal.ts:545) escutam em term.element, e o Linkifier (que fica no filho .xterm-screen) deixa de ver o par. Testei na mesma página: o pane recebe \x1b[<0;11;1M\x1b[<0;11;1m e o link abre uma vez só.

2. O clique reenviado pode cair fora deste terminal (PaneTerminal.tsx:643, :732, :763-765)
O cellAt limita a coluna a [0, cols-1], e o edge só olha as linhas. Se o usuário aperta na última coluna e solta alguns pixels à direita, na mesma linha, então d.cursor fica igual a d.anchor, e click dá true. O par sintético vai para o elemento que estiver sob o ponteiro. Numa aba dividida (SplitView), esse elemento pode ser o xterm do pane vizinho, que reporta o clique ao pane errado. O onMouseDown dele não filtra, porque o evento não é confiável. A mesma troca para term.element resolve, ou então um term.element.contains(event.target) antes de reenviar.

Sugestão para o teste: o caso novo não cobre links. Vale imprimir uma URL no pane, trocar window.open por um stub via addInitScript, clicar nela e conferir que houve uma chamada só.

O que verifiquei e como

  • Li o PaneTerminal.tsx inteiro na parte do mouse, e no xterm 5.5.0 o bindMouse, o SelectionService e o Linkifier.
  • O teste novo falha sem o fix: rodei bun run check run bun scripts/ui-regression.ts em duas cópias do branch (o herdr isolado do próprio check, no chromium_headless_shell do Playwright, não no /opt/google/chrome).
    • Com o fix: PASS a terminal click reaches the pane and a drag sends no click. A suíte parou depois, em the bell's tap takes the permission, um caso de permissão de notificação que não tem relação com este PR e que vem do ambiente headless. Por isso não confirmei os 89 PASS.
    • Sem o fix (o PaneTerminal.tsx do 494c20e): a suíte falha exatamente em Timed out: click as a mouse report.
  • bun run typecheck: ok.
  • Conferi pelo código e não achei problema em:
    • duplo clique (continua selecionando a palavra);
    • reentrada do onMouseUp pelo mouseup sintético (drag já está null);
    • modo observar (o disableStdin corta o envio);
    • toque (nunca chega ao reenvio);
    • clique logo depois de uma seleção (no modo tracking o xterm não estende a seleção, só limpa).

🤖 Generated with Claude Code

…z (GLPI devswha#272)

O clique reenviado ia ao elemento sob o ponteiro. O tratador de links da tela
também escuta ali e abria o link de novo, e um soltar além da borda podia cair
no pane vizinho. Agora vai a term.element. O ui-regression ganha um clique em
link que deve abrir uma vez só (falha sem o fix: abre duas).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gianlukamoraes gianlukamoraes left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 APROVADO — os dois achados da revisão anterior foram corrigidos (GLPI devswha#272, head fda4a50)

🧑 Resumo (para humanos)

  • 🟢 Link abre uma vez só: o clique reenviado agora vai à raiz do terminal, onde o detector de links não escuta. Clicar numa URL num pane com mouse ligado (Claude Code, por exemplo) abre uma aba, não duas.
  • 🟢 O clique não escapa para o pane vizinho: soltar o mouse logo além da borda conta como clique na última coluna deste terminal, nunca no do lado.
  • 🟢 Teste novo para o link, como sugerido: imprime uma URL no pane, clica e confere que abriu exatamente uma vez.
  • 🟡 O teste de tela (navegador real) não rodou nesta máquina: falta uma biblioteca do sistema (libatk) para abrir o Chromium. Typecheck e testes unitários passaram. (não bloqueia)

O que fazer: nada obrigatório. Os critérios estão atendidos; este monitor está em modo revisor e não faz merge.

🤖 Detalhe técnico (file:line, fix, cenário) — para o Claude Code / quem for corrigir

Delta revisado: 95e75c9..fda4a50 (1 commit): src/components/PaneTerminal.tsx:762-768 e o caso novo em scripts/ui-regression.ts:432-469.

Verificado no código:

  • Link uma vez: o par sintético é disparado em term.element. O Linkifier escuta mousedown/mouseup no filho .xterm-screen, e um evento disparado no pai não desce até o filho. O bindMouse do xterm escuta em term.element, então o report SGR continua saindo.
  • Vizinho: com o alvo fixo em term.element, o par só pode chegar a este xterm. A coluna é limitada à área do canvas em MouseService.getMouseReportCoords (node_modules/@xterm/xterm/src/browser/services/MouseService.ts:37-38), então um release fora da borda reporta a última coluna deste pane.
  • Sem recursão: o host (onMouseDown, fase de captura) é ancestral de term.element. Ele vê o mousedown sintético, que tem isTrusted=false, e retorna antes de forçar Shift. O mouseup sintético sobe até o onMouseUp com drag === null e retorna. Nada mudou aqui em relação ao head anterior.
  • root nulo: antes de term.open o term.element é undefined, mas aí não há drag. O guard && root cobre o caso.

Gate: pr-gate.sh 24 → rc=2 GATE SETUP FAILED: sem matriz para dataagile/herdr-web-ui. Isso não é verdict. Rodei à mão num worktree no head fda4a50:

  • bun run typecheck → ok
  • bun run test:unit → 2194 pass, 6 skip, 0 fail (2200 testes)
  • bun run check run bun scripts/ui-regression.ts → não rodou: /opt/google/chrome/chrome não existe aqui, e com CHROME_PATH apontando para o Chromium do Playwright o processo sai com libatk-1.0.so.0: cannot open shared object file. Não confirmei o PASS a link in the terminal opens once nem que ele falha sem o fix. Vale rodar no ambiente do autor antes do merge.

Nota (não bloqueia): o [N-1] da revisão de 27/10 (um tremor que passa para a célula vizinha vira seleção e não clique) continua como estava. Só vale mexer se aparecer reclamação.

Checks de UI: nenhum campo de texto editável alterado e nenhuma regra de validação nova. A evidência é o teste em navegador real em scripts/ui-regression.ts.

@gianlukamoraes

Copy link
Copy Markdown
Author

Revisão do #24 (head fda4a50)

Veredito: ok para merge. O último commit resolve os dois achados da revisão anterior. Sobrou um ponto pequeno, que não bloqueia.

A revisão anterior foi resolvida?

  • Link abria duas vezes: sim. O par sintético agora vai para term.element (src/components/PaneTerminal.tsx:764-768). O Linkifier escuta no .xterm-screen (node_modules/@xterm/xterm/src/browser/Terminal.ts:494, Linkifier.ts:55-56), que é filho da raiz, então não vê mais o par. O bindMouse (Terminal.ts:774) e o SelectionService (Terminal.ts:545) escutam na raiz e continuam recebendo.
  • Clique caindo no pane vizinho: sim. O evento sai na raiz deste terminal. Um clientX além da borda é limitado pelo getMouseReportCoords (MouseService.ts:33-36), então o report fica na última coluna deste pane.

Achado (não bloqueia)

1. O clique reenviado perde os modificadores (src/components/PaneTerminal.tsx:766)
O init só leva button e as coordenadas, então ctrlKey, altKey, shiftKey e metaKey vão como false. O sendEvent do xterm copia esses campos para o report (Terminal.ts:673-675).

  • Ctrl+clique (ou Alt+clique fora do Mac) chega ao programa como clique simples: no SGR vai \x1b[<0;…M em vez de \x1b[<16;…M.
  • Shift+clique (Option+clique no Mac) antes nunca chegava ao pane, que é a convenção de terminal para "não mande este clique ao programa". Agora chega como clique simples. Na main esse clique não ia ao pane.

Sugestão: copiar os modificadores do mouseup real, que não é alterado (o defineProperty só mexe no mousedown):

const init = { bubbles: true, cancelable: true, button: 0, clientX: event.clientX, clientY: event.clientY,
  ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey };

Com Shift (ou Option no Mac) de verdade, o shouldForceSelection do xterm barra o report do par sintético (Terminal.ts:781), e o bypass volta a ser o da main. Ctrl e Alt passam a ir no report.

O que verifiquei e como

  • Li o diff inteiro e, em PaneTerminal.tsx, toda a parte do mouse (:600-822), o linkHandler/linkPressed (:364-395) e o guard do onData (:1047). No xterm 5.5.0 li o bindMouse, o SelectionService.handleMouseDown, o Linkifier e o MouseService.
  • Rodei bun run check run bun scripts/ui-regression.ts (Chromium do Playwright 1243) em duas cópias:
    • head fda4a50: PASS a terminal click reaches the pane and a drag sends no click e PASS a link in the terminal opens once. Na 2ª rodada a suíte inteira passou: 90 PASS, exit 0. Na 1ª ela parou em scripts/terminal-copy-regression.ts:323 (a selecting drag's wheel scrolls herdr through the API, never as input): chegou um \x1b[<35;16;6M, o report de movimento do mouse.move feito antes do press, que caiu depois do beforeWheel. Nem o arquivo nem o caminho do arrasto mudam neste PR, e a 2ª rodada passou. Parece intermitência do teste (a espera de 100 ms em :308), não regressão.
    • head com o PaneTerminal.tsx de 95e75c9 (antes do último commit): o teste do clique passa e o do link falha com opened = [url, url]. Então o teste novo pega o bug do link aberto duas vezes.
    • O teste do clique falhar sem o fix original (Timed out: click as a mouse report) já tinha sido confirmado na revisão anterior.
  • bun run typecheck: ok. bun run test:unit: 2194 pass, 6 skip, 0 fail.
  • Conferi pelo código, sem problema:
    • sem recursão: o mousedown sintético tem isTrusted=false e volta em :702; o mouseup sintético encontra drag já null.
    • os listeners de documento que o xterm registra no mousedown sintético saem no mouseup sintético, porque buttons é 0.
    • nada em scripts/ ou src/ dispara mousedown sintético, então o guard !event.isTrusted não muda outro caminho.
    • modo observar, prompt de senha e pane held continuam cortando o report no onData (:1047).
    • CHANGELOG em [Unreleased] / ### Fixed, com o link do PR.

🤖 Generated with Claude Code

@gianlukamoraes
gianlukamoraes merged commit fedfd5f into main Oct 28, 2026
@gianlukamoraes
gianlukamoraes deleted the fix/glpi-272 branch October 28, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-owner Reviewed by pr-monitor; waiting for a human to decide or merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant