Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ por trás, assinaturas existentes e lançamento conjunto. Isso amplia as etapas
usa hooks para conversa e permissões na Bee e retoma o ID nativo. Mensagem real
curta comprovada; prompts nativos sem hook são sinalizados e concluídos por
`Ctrl+O` (#29); a origem das ações é explícita (#30); histórico e exportação
vêm da transcrição nativa (#31). Ferramentas/perfis reais exigem ensaio humano.
vêm da transcrição nativa (#31); editor multilinha e rolagem (#32).
Ferramentas/perfis reais exigem ensaio humano.
3. Experiência integrada aos dois harnesses, histórico e exportação: pendente.
4. Troca real entre agentes/contas com revisão e preservação da origem: pendente.
5. Lançamento conjunto após todos os aceites, sem substituir o requisito por
Expand Down
31 changes: 30 additions & 1 deletion docs/EXECUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Próximo: ensaio humano de ferramentas/permissões reais
([#30](https://github.com/lippdev/memory-bee/issues/30)) e histórico/exportação
revisável ([#31](https://github.com/lippdev/memory-bee/issues/31)).
O estado ainda guarda só IDs; histórico e exportação vêm da transcrição nativa
(#31, PR em andamento). Perfis reais e Codex integrado seguem pendentes.
(#31) e a conversa tem editor multilinha e rolagem (#32). Perfis reais e Codex integrado seguem pendentes.
O lançamento da experiência completa ainda exige os dois agentes (ADR 0018).

**Refinamento visual.** Referência HTML e [plano visual](specs/workspace-visual.md)
Expand Down Expand Up @@ -845,3 +845,32 @@ escopo da entrega atual nem a próxima tarefa aprovada.
Exportação após o Claude encerrar é feita pela CLI `export`, não pela TUI.
Código selecionado (v2) não é oferecido na TUI.
- Próximo: ensaios humanos 52 e 54; depois Codex real no workspace (ADR 0018).

## Claude oculto — editor e navegação da conversa (#32, código)

- Data: 2026-09-28. PR #52 (#31) integrado por squash em `1871931`; branch da
sessão recomeçado da `main`.
- Novo `src/tui/draft.rs`: rascunho multilinha com cursor por caractere,
colagem que preserva quebras e descarta controles, e quebra por largura de
exibição (inclui caracteres largos). `workspace --claude` passa a usar esse
editor (`Alt+Enter`/`Ctrl+J`, setas, Home/End, Delete), mantém o rascunho em
permissões, exportação, tela original e resize, e envia mensagens de várias
linhas como colagem delimitada quando o Claude a ativou (sem ela, quebras viram
espaços). A conversa quebra linhas longas (antes cortadas), rola com
`PgUp`/`PgDn` e ↑/↓ nas bordas do rascunho, avisa novas mensagens abaixo e
volta ao fim com `Esc` ou ao enviar. Sem cor usa o fundo padrão do terminal.
Atalhos se distribuem em até quatro linhas; mínimo passou de 40×10 para 40×12
para mantê-los todos visíveis.
- Evidências: testes unitários do editor (colunas entre linhas, colagem,
quebra com caracteres largos e posição do cursor); `check_claude_hidden_pty.py`
com CLI falsa que registra a entrada recebida: rolagem com `PgUp` e `Esc`,
colagem não enviada antes de Enter, edição com Home/End/`Alt+Enter`, envio
delimitado exato e envio em uma linha sem colagem delimitada, em 80×24 e 40×12.
Telas conferidas em emulador (`pyte`), o que revelou e corrigiu o cursor fora da
caixa em 40×12. Checks canônicos, scripts PTY e `check_bundle.py` passaram
localmente (Linux). Autorrevisão.
- Limitações: `Alt+Enter` depende do terminal; `Ctrl+J` é a alternativa. Ensaio
em terminais reais pendente (item 55). A rolagem é por mensagem, não por linha
quebrada.
- Próximo: #33 (Codex App Server autenticado), que também exige ensaio com conta
real, ou #41 (recuperação do estado após falha).
18 changes: 18 additions & 0 deletions docs/MANUAL_TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -952,3 +952,21 @@ impresso. Não versionar pacotes com dados reais.
| Item novo | Estado | Evidência manual |
|---|---|---|
| 54: reidratação na retomada, exportação com exclusões, segredos e verify | Pendente | — |

## 55. Editor e navegação do Claude oculto (pendente)

Em terminais reais (Terminal.app, iTerm2 e um Linux), abrir `workspace --claude`
num projeto descartável já confiado. Digitar uma mensagem, usar `Alt+Enter` (e
`Ctrl+J`, se `Alt+Enter` não chegar) para quebrar linha, mover com setas e
Home/End e corrigir com Backspace/Delete. Colar um texto de várias linhas:
nada deve ser enviado antes de Enter. Enviar e conferir no Claude original
(`Ctrl+O`) que a mensagem chegou inteira, com as quebras. Gerar uma resposta
longa, rolar com `PgUp`/`PgDn`, conferir o título "lendo anteriores"/"novas
abaixo" e voltar com `Esc`. Com rascunho digitado, provocar um pedido de
permissão e redimensionar a janela: o rascunho deve continuar. Repetir com
`--no-color` e em 40×12, conferindo todos os atalhos visíveis. Sair com `Ctrl+Q`
e após um erro do Claude, verificando a restauração do terminal.

| Item novo | Estado | Evidência manual |
|---|---|---|
| 55: editor multilinha, colagem, rolagem, sem cor e 40×12 em terminais reais | Pendente | — |
17 changes: 16 additions & 1 deletion docs/specs/workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ O Claude continua controlando login, sessão e execução. Um socket Unix em pas
temporária 0700 recebe eventos; o subprocesso de hook devolve decisão `allow` ou
`deny` ao Claude. Falha de ponte e timeout negam permissões. A tela nativa fica
oculta; a Bee mostra mensagens, nomes de ações e decisão `y`/`n`, com negação
inicial. Enter envia prompt, `Ctrl+C` interrompe e `Ctrl+Q` encerra. O terminal
inicial. Enter envia prompt, `Ctrl+C` interrompe e `Ctrl+Q` encerra (editor e
rolagem descritos abaixo). O terminal
volta ao estado anterior na saída. `--claude-terminal` reabre o modo anterior com
a TUI nativa visível para diagnóstico. Se houver permissão pendente, `Ctrl+C` e
`Ctrl+Q` negam antes de interromper ou sair; outras teclas além de `y`, `n` e
Expand Down Expand Up @@ -76,6 +77,20 @@ processo ativo antes de remover a trava antiga. `--resume` usa o último ID dest
pasta; não reenvia prompt. A sessão nativa pode não existir quando se sai antes
de enviar uma mensagem. Isolamento de perfis e integração Codex estão pendentes.

**Editor e navegação (#32).** O rascunho tem várias linhas: `Alt+Enter` (ou
`Ctrl+J`) quebra linha; setas, Home/End, Backspace e Delete editam; a colagem
entra no rascunho com as quebras preservadas e nunca é enviada sem Enter. O
rascunho continua ao abrir permissões, exportação, a tela original ou ao
redimensionar, e pode ser escrito antes de o Claude ficar pronto. Ao enviar,
mensagens com várias linhas vão ao Claude como uma única colagem delimitada
quando o terminal dele a ativou; sem isso, as quebras viram espaços para não
enviar pedaços. A conversa quebra linhas pela largura de exibição; `PgUp`/`PgDn`
(ou ↑/↓ fora do rascunho) leem mensagens anteriores, o título indica novas
mensagens abaixo e `Esc` ou enviar volta ao fim. A caixa de mensagem mostra até
cinco linhas sem esconder a conversa. Sem cor, a tela usa as cores padrão do
terminal. O tamanho mínimo é 40×12; os atalhos ocupam até quatro linhas para
continuarem legíveis nessa largura.

**Histórico e exportação (#31).** A transcrição vem do campo `transcript_path`
dos hooks oficiais; a Bee aceita só arquivo comum, sem symlink, chamado
`<id-da-sessão>.jsonl`, e não guarda o caminho no estado. Na retomada, ao receber
Expand Down
82 changes: 78 additions & 4 deletions scripts/check_claude_hidden_pty.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,68 @@ def hook(name, **fields):
sys.exit(0)
'''

# Editor checks: records exactly what Claude would receive on its input.
FAKE_EDITOR = r'''#!/usr/bin/env python3
import json, os, signal, subprocess, sys
signal.signal(signal.SIGINT, signal.SIG_IGN) # Real Claude reads Ctrl+C as a key.
args = sys.argv[1:]
session_id = args[args.index('--session-id') + 1]
if os.environ.get('BEE_BRACKETED'):
sys.stdout.write('\x1b[?2004h')
sys.stdout.flush()
def hook(name, **fields):
payload = json.dumps(dict(hook_event_name=name, session_id=session_id, **fields))
subprocess.run([os.environ['BEE_BINARY'], '__claude-hook'], input=payload, text=True, capture_output=True, check=True)
hook('SessionStart')
hook('MessageDisplay', delta='\n'.join(f'linha-{i:02d}' for i in range(60)), final=True, index=0)
for line in sys.stdin:
line = line.rstrip('\n')
with open(os.environ['BEE_FAKE_STDIN'], 'a') as f:
f.write(repr(line) + '\n')
if line == '/exit':
sys.exit(0)
if not line.startswith('\x1b[200~') and not (os.environ.get('BEE_BRACKETED') and '\x1b[201~' not in line):
hook('Stop') # The turn ends once the whole message arrived.
'''


def editor(binary, project, state, env, size, bracketed):
rows, cols = size
master, slave, before, proc, output, until = spawn(binary, project, state, env, size=size)
log = Path(env['BEE_FAKE_STDIN'])
try:
until(b'linha-59')
os.write(master, b'\x1b[5~') # PgUp reads older messages.
until(b'anteriores')
os.write(master, b'\x1b') # Esc follows the end again.
time.sleep(0.3)
os.write(master, b'\x1b[200~linha um\nlinha dois\x1b[201~')
time.sleep(0.5)
assert not log.exists(), 'paste must not be sent before Enter'
os.write(master, b'\x1b[H>') # Home, then edit the second line.
os.write(master, b'\x1b[F\x1b\r') # End, then Alt+Enter adds a line.
os.write(master, 'três'.encode())
os.write(master, b'\r')
until(b'Turno') # Stop arrived: Ctrl+Q may type /exit at the idle prompt.
os.write(master, b'\x11')
code = drain_until_exit(master, proc, output, 10)
assert code == 0, (code, log.exists() and log.read_text())
assert termios.tcgetattr(slave) == before
assert b'Ctrl+Q' in output, 'shortcuts stay visible at this width'
received = [eval(line) for line in log.read_text().splitlines()]
if bracketed:
expected = ['\x1b[200~linha um', '>linha dois', 'três\x1b[201~', '/exit']
else:
expected = ['linha um >linha dois três', '/exit']
assert received == expected, received
log.unlink()
finally:
if proc.poll() is None:
proc.kill()
proc.wait()
os.close(master)
os.close(slave)


def history_and_export(binary, project, state, env, resume):
master, slave, before, proc, output, until = spawn(binary, project, state, env, resume)
Expand Down Expand Up @@ -118,9 +180,9 @@ def drain_until_exit(master, proc, output, seconds):
return proc.returncode


def spawn(binary, project, state, env, resume=False):
def spawn(binary, project, state, env, resume=False, size=(24, 80)):
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 80, 0, 0))
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', size[0], size[1], 0, 0))
before = termios.tcgetattr(slave)
argv = [str(binary), 'workspace', '--claude', '--project', str(project), '--state', str(state), '--no-color']
if resume:
Expand Down Expand Up @@ -203,7 +265,7 @@ def until(token):
try:
until(b'pronto.')
os.write(master, b'hello\r')
until(b'Permitir esta')
until(b'Permitir')
os.write(master, decision.encode())
until(b'sintetica')
until(b'regras') # Read ran without a request: Claude's rules, not the Bee.
Expand Down Expand Up @@ -291,7 +353,19 @@ def main():
history_and_export(binary, project, state, env, True)
# History is shown from the transcript, never typed back into Claude.
assert stdin_log.read_text().splitlines() == ['/exit', '/exit']
print('PASS: Bee-only UI, hidden original output, hooks, deny/allow, Ctrl+Q denial, resume, blocked native setup via Ctrl+O, safe Ctrl+Q, history rehydration, verified export and terminal restoration')
with tempfile.TemporaryDirectory(prefix='bee-editor-') as temp:
root = Path(temp)
fake_dir = root / 'bin'
fake_dir.mkdir()
fake = fake_dir / 'claude'
fake.write_text(FAKE_EDITOR)
fake.chmod(0o700)
project = root / 'project'
project.mkdir()
env = dict(os.environ, PATH=f'{fake_dir}:/usr/bin:/bin', BEE_BINARY=str(binary), BEE_FAKE_STDIN=str(root / 'stdin'))
editor(binary, project, root / 'state-a', dict(env, BEE_BRACKETED='1'), (24, 80), True)
editor(binary, project, root / 'state-b', env, (12, 40), False) # Documented minimum width.
print('PASS: Bee-only UI, hidden original output, hooks, deny/allow, Ctrl+Q denial, resume, blocked native setup via Ctrl+O, safe Ctrl+Q, history rehydration, verified export, multiline editor, scrolling, 40-column controls and terminal restoration')


if __name__ == '__main__':
Expand Down
Loading
Loading