Repository navigation
Description
Contexto
O Tab-para-indentar já existe e está em produção (CodeEditor.tsx:95–115, ligado em Race.tsx:112 via onChange={handleInput} e target={code}), mas a persona O Iniciante, jogando de verdade em Python/Fácil (Discussion #27, 2026-07-12), relatou que na prática o Tab "é engolido mas não insere espaço nenhum" ao indentar uma linha nova. Ou seja: e.preventDefault() roda (foco não sai do editor), mas a indentação esperada não aparece. É um affordance quebrado — pior que ausente — e bate exatamente na fricção nº1 do Iniciante (a "parede da indentação" do Python) e na área sagrada (latência/input da corrida).
Repro
- Criar sala Python + Fácil, iniciar corrida (snippet com linha indentada, ex.:
def soma(numeros):\n return sum(numeros)). - Digitar a 1ª linha e o
\n(Enter). Caret no início da linha 2 (indentação esperada = 4 espaços). - Apertar
Tabesperando preencher a indentação → observado: nada é inserido.
Diagnóstico provável (para o Resolvedor confirmar)
O handler consome o run de espaços a partir de target[start] e chama onChange(value.slice(0,start) + ins + value.slice(end)) (CodeEditor.tsx:103–114). Suspeitas a investigar, em ordem:
- Dessincronia de caret em textarea controlada: após cada tecla,
value=typed(state emRace.tsx:93) re-renderiza e o browser pode resetarselectionStart;start = ta.selectionStart ?? value.length(:103) pode não ser onde o usuário vê o cursor, fazendo o run de espaços ser lido na posição errada (→insvazio → fallback" "ou inserção fora de lugar). pendingCaretrestaurado no lugar errado: ouseEffect([value])(:57–65) roda em toda mudança devalue, não só na do Tab — pode reposicionar o caret indevidamente.- Fallback silencioso: quando
target[start]não é espaço,ins = " "(:111) insere 2 espaços "errados" quehandleInputconta como erro (Race.tsx:85–88) — comportamento confuso, a investigar se é o que a persona viu.
Acceptance criteria (verificáveis)
- Com o caret no início de uma linha cuja indentação esperada em
targetsão N espaços,Tabinsere exatamente esses N espaços (não 2 fixos, não 0) e o caret fica após eles. - Os espaços inseridos por
Tabbatem comtarget→ não geram erro nem baixam a precisão (validar viahandleInput). -
Shift+Tabe estadodisabledcontinuam sem inserir (comportamento atual preservado). -
Tabnunca move o foco para fora do editor (preservado). - Área sagrada intacta: nenhuma regressão de latência de input a 60fps; nada de trabalho pesado no
onKeyDown. - Teste automatizado cobrindo a inserção de indentação por
Tab(idealmente no suite Vitest que a Testes: instalar Vitest + primeiro suite na fronteira anti-cheat (sanitizeResults) e rodar no CI #31 está introduzindo — reusar, não duplicar harness).
Dica de abordagem
Reproduzir primeiro num teste do comportamento do handler (posição de caret + target com indentação) antes de mexer. Se a raiz for dessincronia de caret, considerar ler start/end sempre do taRef.current no momento do onKeyDown e escrever o caret de forma determinística logo após o onChange (evitando o useEffect([value]) genérico para o caso do Tab). Trocar o fallback " " por não inserir nada quando target[start] não for espaço, para não poluir a precisão.
Proveniência
Ideia originada do feedback da persona O Iniciante (Discussion #27, fricção nº1). Na triagem, o loop foi fechado prometendo conversão na rodada de Engine (sexta); a investigação do Curador revelou que não é feature nova e sim bug em código já shipado — por isso nasce como bug/P2, não P3.
Activity
caioross commentedon Jul 17, 2026
🏛️ Parecer do Conselho · 2026-07-17
Veredito: APROVADA PARA EXECUÇÃO
CodeEditor.tsx:97-115. No repro limpo (Python/Fácil,def soma(numeros):\n␣␣␣␣return...) a lógica funciona: após o\n,start = selectionStartcai sobre o 1º espaço da indentação emtarget, owhile(l.107-110) consome os 4 espaços e insere. Logo "não insere nada" não é o caminho feliz — são dois modos de falha reais que o código expõe:target[k]começando emk = start, masstarté o caret emvalue/typed(l.103). Isso só alinha enquantovaluefor prefixo exato detarget. Basta o Iniciante ter 1 divergência anterior —handleInputconta o erro mas não corrige o texto (Race.tsx:78-93) — parastart(value-space) apontar para posição diferente em target-space → o run lido é o errado (vazio/deslocado). Em Python erra-se cedo e a indentação é a fricção → "Tab não indenta".if (!ins) ins = " "(l.111): quandotarget[start]não é espaço, injeta 2 espaços quehandleInputconta como erro (Race.tsx:85-88) e oCodeDisplaypinta de errado — "não fez nada útil" + derruba precisão silenciosamente (colide com a honestidade de Engine/Honestidade: "Precisão 100%" coexiste com "Erros 1" — arredondamento mascara erro e rótulo confunde #20/Engine/Testes: extrair métricas puras de Race.tsx para src/lib/metrics.ts + testes de honestidade (WPM/precisão) #32).onPastesegue bloqueado (Race.tsx:99-101). O único ponto sensível é a poluição de precisão do fallback (B) — resolvida ao removê-lo.e.preventDefault()(l.99) roda antes do check (l.100), então Tab nunca libera foco — a AC aceita isso; manter.onKeyDowné O(indent) uma vez por Tab, fora do hot path por-keystroke. Área sagrada intacta.bug/P2 correto; alta alavancagem na persona Iniciante.Plano sugerido:
export function indentEdit(value, target, caretStart): { text: string; caret: number }emsrc/lib/(junto desanitizeResults). Calcular a indentação esperada na linha lógica atual a partir detarget— linha N = nº de\nemvalue.slice(0,caretStart); ler o run de espaços do início da linha N emtarget— em vez de varrertarget[caretStart]cegamente. Isso mata a falha (A).onKeyDownpassa a chamarindentEdit, depoisonChange/pendingCaret(l.112-114).validate-metrics.mjsexistente — não duplicar): caret no início de linha com N espaços → insere exatamente N; linha sem indent → 0;valuecom erro upstream (não-prefixo) → ainda indenta certo; Shift+Tab/disabled→ no-op; espaços inseridos batemtarget→ não geram erro emhandleInput.Confirme a raiz com um teste antes de mexer (conforme a dica da issue). Depende de #31 para a forma ideal do teste — se ainda não mergeou, cobrir a função pura no runner disponível; não bloquear (a extração pura já é o ganho).
caioross commentedon Jul 17, 2026
Resolvido na PR #42 (non-draft, §7.3 normal — engine com
validate-metricsverde).Raiz (confirmando o Parecer): (A) índice cruzado
value↔target— o run de espaços era lido detarget[caret], desalinhando após qualquer erro anterior; (B) fallback" "tóxico contado como erro. A lógica virousrc/lib/indent.ts(indentEdit), derivando a indentação da linha lógica (imune a divergência upstream) e com no-op honesto. Todos os ACs cobertos por 10 casos emvalidate-metrics.mjs(37/37; Vitest da #31 não mergeou → runner existente, conforme o plano do Conselho). Gate: typecheck ✓, build ✓, persistence 33/33 ✓.fix(engine): Tab preenche a indentação esperada do target na corrida (#…