diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index eba88f14..0a5760ca 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-01T09:41:42Z", + "exported_at": "2026-05-01T10:01:22Z", "decisions": [ { "id": "D-1", diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index 1112e9c4..e92f5e02 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -66,6 +66,7 @@ class _ClaudePaneState extends State { @override void dispose() { _resizeTimer?.cancel(); + _flushTimer?.cancel(); _eventSub?.cancel(); _eventSub = null; final id = _paneId; @@ -122,17 +123,10 @@ class _ClaudePaneState extends State { ? primarySessionName(repoRoot) : secondarySessionName(repoRoot, widget.secondaryIndex!); - // Try tmux-wrapped first (persistence). Fall back to direct claude - // if tmux spawn errors. - var argv = [ - 'tmux', - 'new-session', - '-A', - '-s', - sessionName, - '--', - 'claude', - ]; + // TODO: restore tmux+claude once resize is stable. + // Bare shell for resize debugging. + final shell = Platform.environment['SHELL'] ?? '/bin/zsh'; + var argv = [shell, '-l']; var resp = await ipc.request('pane.spawn', args: { 'argv': argv, 'kind': PaneKind.claude.wire, @@ -172,6 +166,16 @@ class _ClaudePaneState extends State { setState(() {}); } + final _outputBuf = StringBuffer(); + Timer? _flushTimer; + + void _flushOutput() { + _flushTimer = null; + if (_outputBuf.isEmpty) return; + _terminal.write(_outputBuf.toString()); + _outputBuf.clear(); + } + void _subscribe() { final kernel = _kernel(); if (kernel == null) return; @@ -181,7 +185,12 @@ class _ClaudePaneState extends State { case 'pane.output': final b64 = e.data['bytes_b64']; if (b64 is String) { - _terminal.write(utf8.decode(base64Decode(b64), allowMalformed: true)); + _outputBuf.write(utf8.decode(base64Decode(b64), allowMalformed: true)); + // Throttle writes to ~60fps. Multiple PTY reads within a + // 16ms window are batched into one terminal.write() call, + // preventing split escape sequences from rendering as + // garbage between frames. + _flushTimer ??= Timer(const Duration(milliseconds: 16), _flushOutput); } case 'pane.exit': if (widget.isPrimary) { diff --git a/lib/src/pty/native_pty.dart b/lib/src/pty/native_pty.dart index 3effac60..216f4cd9 100644 --- a/lib/src/pty/native_pty.dart +++ b/lib/src/pty/native_pty.dart @@ -236,7 +236,7 @@ class NativePty { ffi.Int32 Function(ffi.Pointer<_Pollfd>, ffi.Uint32, ffi.Int32), int Function(ffi.Pointer<_Pollfd>, int, int)>('poll'); - final buf = malloc(4096); + final buf = malloc(65536); final pfd = calloc<_Pollfd>(); pfd.ref.fd = fd; pfd.ref.events = 0x0001; // POLLIN @@ -249,7 +249,7 @@ class NativePty { if (pfd.ref.revents & 0x0038 != 0 && pfd.ref.revents & 0x0001 == 0) { break; } - final n = rd(fd, buf.cast(), 4096); + final n = rd(fd, buf.cast(), 65536); if (n <= 0) break; port.send(Uint8List.fromList(buf.asTypedList(n))); }