diff --git a/CHANGELOG.md b/CHANGELOG.md index 28ef6687..d14b24a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,12 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. opens a picker in the interaction zone with the CLI's model list and the current model marked. A rejected name rolls back and raises a toast. (T-408) +### Removed + +- **tmux is no longer a required tool.** clide stopped spawning tmux when Claude + session persistence moved to `--resume` (D-77); the toolchain no longer probes + for it or warns when it's absent, on any platform. + ## [2.4.1] — 2026-06-12 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 43090b7e..163c203e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ An IDE for Claude Code CLI. Single Flutter package at the repo root. - **`lib/`** — all Dart code. Subsystem handlers (`lib/src/daemon/`, `lib/src/pty/`, `lib/src/ipc/`, `lib/src/git/`, `lib/src/pql/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), and the extension framework (`lib/extension/`). The Flutter app hosts the IPC server in-process (D-56). PTY spawning uses Dart FFI `posix_openpt()` + `posix_spawn()` directly. - **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it. -tmux owns Claude session persistence (D-41) — the app re-attaches on restart via `tmux new-session -A`. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages. +Claude session persistence is `--resume ` against Claude Code's transcript files (D-77, superseding the original tmux-backed D-41) — the app re-attaches on restart, no tmux required. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages. Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`governance/`](governance/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`governance/README.md`](governance/README.md)). Python Textual predecessor under [`legacy/`](legacy/). diff --git a/lib/kernel/src/toolchain.dart b/lib/kernel/src/toolchain.dart index 8b02cf48..7bc18dc6 100644 --- a/lib/kernel/src/toolchain.dart +++ b/lib/kernel/src/toolchain.dart @@ -21,7 +21,6 @@ export 'toolchain_paths.dart'; class Toolchain extends ChangeNotifier implements ToolchainView { String? _git; String? _pql; - String? _tmux; String? _shell; Map? _gitEnv; bool _resolved = false; @@ -31,8 +30,6 @@ class Toolchain extends ChangeNotifier implements ToolchainView { @override String get pql => _pql ?? 'pql'; @override - String get tmux => _tmux ?? 'tmux'; - @override String get shell => _shell ?? (Platform.isWindows ? 'powershell.exe' : '/bin/bash'); /// Extra environment variables for git (e.g. GIT_EXEC_PATH for dugite). @@ -45,13 +42,7 @@ class Toolchain extends ChangeNotifier implements ToolchainView { bool get allOk => _resolved && missing.isEmpty; @override - List get missing => [ - if (_git == null) 'git', - if (_pql == null) 'pql', - // tmux has no Windows build; its absence there is the documented - // no-tmux mode, not a missing tool. - if (_tmux == null && !Platform.isWindows) 'tmux', - ]; + List get missing => [if (_git == null) 'git', if (_pql == null) 'pql']; /// Returns a Future that completes when resolution finishes. Future waitForResolution() { @@ -72,7 +63,6 @@ class Toolchain extends ChangeNotifier implements ToolchainView { void applyResolved(ResolvedPaths p) { _git = p.git; _pql = p.pql; - _tmux = p.tmux; _shell = p.shell; _gitEnv = p.gitEnv; _resolved = true; diff --git a/lib/kernel/src/toolchain_paths.dart b/lib/kernel/src/toolchain_paths.dart index c9e78925..b6d42027 100644 --- a/lib/kernel/src/toolchain_paths.dart +++ b/lib/kernel/src/toolchain_paths.dart @@ -12,11 +12,10 @@ import 'dart:io'; /// Serializable result of tool resolution (crosses isolate boundary). class ResolvedPaths { - const ResolvedPaths({this.git, this.pql, this.tmux, this.shell, this.gitEnv}); + const ResolvedPaths({this.git, this.pql, this.shell, this.gitEnv}); final String? git; final String? pql; - final String? tmux; final String? shell; final Map? gitEnv; } @@ -32,7 +31,6 @@ abstract class ToolchainView { String get git; String get pql; - String get tmux; String get shell; Map? get gitEnv; bool get resolved; @@ -50,8 +48,6 @@ class _StaticToolchain implements ToolchainView { @override String get pql => _paths.pql ?? 'pql'; @override - String get tmux => _paths.tmux ?? 'tmux'; - @override String get shell => _paths.shell ?? (Platform.isWindows ? 'powershell.exe' : '/bin/bash'); @override Map? get gitEnv => _paths.gitEnv; @@ -60,13 +56,7 @@ class _StaticToolchain implements ToolchainView { @override bool get allOk => missing.isEmpty; @override - List get missing => [ - if (_paths.git == null) 'git', - if (_paths.pql == null) 'pql', - // tmux has no Windows build; its absence there is the documented - // no-tmux mode, not a missing tool. - if (_paths.tmux == null && !Platform.isWindows) 'tmux', - ]; + List get missing => [if (_paths.git == null) 'git', if (_paths.pql == null) 'pql']; } /// Top-level function for compute/isolate use. Returns a plain-data @@ -89,7 +79,7 @@ ResolvedPaths resolveToolchainPaths() { git = _findOnPath('git'); } - return ResolvedPaths(git: git, pql: _findOnPath('pql'), tmux: _findOnPath('tmux'), shell: _resolveShell(), gitEnv: gitEnv); + return ResolvedPaths(git: git, pql: _findOnPath('pql'), shell: _resolveShell(), gitEnv: gitEnv); } /// The user's interactive shell. POSIX honours `$SHELL`; Windows has diff --git a/lib/src/pty/env.dart b/lib/src/pty/env.dart index 78eed5b9..f98dd5f4 100644 --- a/lib/src/pty/env.dart +++ b/lib/src/pty/env.dart @@ -41,7 +41,7 @@ const Map clidePtyEnvDefaults = { 'COLORTERM': 'truecolor', // Encourages 24-bit emission from tooling that checks this: 'CLICOLOR_FORCE': '1', - // tmux inherits these when clide spawns tmux; safe to propagate. + // UTF-8 locale for the child and anything it spawns; safe to propagate. 'LANG': 'en_US.UTF-8', 'LC_ALL': 'en_US.UTF-8', }; diff --git a/lib/src/pty/pty.dart b/lib/src/pty/pty.dart index 2c5691ed..b3f99b92 100644 --- a/lib/src/pty/pty.dart +++ b/lib/src/pty/pty.dart @@ -1,7 +1,7 @@ /// PTY subsystem — spawn child processes under a PTY and expose their /// output as a byte stream. POSIX uses posix_openpt() + posix_spawn(); /// Windows uses ConPTY. Desktop IDE's pane model (terminal / Claude / -/// future tmux wrappers) rides on this. +/// future PTY-backed panes) rides on this. library; export 'env.dart' show clidePtyEnvDefaults, mergePtyEnv; diff --git a/lib/test_app.dart b/lib/test_app.dart index e542b8cb..76dcf87a 100644 --- a/lib/test_app.dart +++ b/lib/test_app.dart @@ -113,22 +113,17 @@ class _ClideTestAppState extends State { _say('--- toolchain ---'); _log('toolchain.git', tc.git); _log('toolchain.pql', tc.pql); - _log('toolchain.tmux', tc.tmux); _log('toolchain.shell', tc.shell); _log('toolchain.missing', tc.missing.isEmpty ? 'none' : tc.missing.join(', ')); _say(''); await _testExists('git', tc.git); await _testExists('pql', tc.pql); - // tmux has no Windows build — its absence there is the documented - // no-tmux mode, so the probes would only report a non-failure. - if (!Platform.isWindows) await _testExists('tmux', tc.tmux); await _testExists('shell', tc.shell); _say(''); await _testExec('git --version', tc.git, ['--version'], workDir); await _testExec('pql --version', tc.pql, ['--version'], workDir); - if (!Platform.isWindows) await _testExec('tmux -V', tc.tmux, ['-V'], workDir); // PowerShell has no --version flag; ask for the version variable // through the same -c path the passthrough tests use. await _testExec('shell --version', tc.shell, Platform.isWindows ? ['-c', r'$PSVersionTable.PSVersion.ToString()'] : ['--version'], workDir); @@ -141,7 +136,6 @@ class _ClideTestAppState extends State { String shellCall(String exe, String args) => Platform.isWindows ? "& '$exe' $args" : '$exe $args'; await _testExec('shell -c git', tc.shell, ['-c', shellCall(tc.git, '--version')], workDir); await _testExec('shell -c pql', tc.shell, ['-c', shellCall(tc.pql, '--version')], workDir); - if (!Platform.isWindows) await _testExec('shell -c tmux', tc.shell, ['-c', shellCall(tc.tmux, '-V')], workDir); await _testExec('shell -c git (bare)', tc.shell, ['-c', 'git --version'], workDir); _say(''); diff --git a/lib/widgets/src/multitab_pane.dart b/lib/widgets/src/multitab_pane.dart index f66e5fd2..26228043 100644 --- a/lib/widgets/src/multitab_pane.dart +++ b/lib/widgets/src/multitab_pane.dart @@ -16,7 +16,7 @@ typedef MultitabEntryCallback = void Function(MultitabEntry entry); /// /// The widget is generic and domain-free: it never knows what's /// inside a tab. Hosts pick `T` and decide what add / close mean -/// (e.g. spawning or killing a tmux session for the Claude pane). +/// (e.g. spawning or killing a Claude session for the Claude pane). /// /// See `docs/design/multitab-pane.md` for the design rationale. class MultitabPane extends StatelessWidget { diff --git a/test/builtin/ipc_status/widget_test.dart b/test/builtin/ipc_status/widget_test.dart index 59a7caf8..89692903 100644 --- a/test/builtin/ipc_status/widget_test.dart +++ b/test/builtin/ipc_status/widget_test.dart @@ -39,20 +39,20 @@ void main() { }); testWidgets('all-tools-resolved shows a single "application ok" chip', (tester) async { - f.services.toolchain.applyResolved(const ResolvedPaths(git: '/usr/bin/git', pql: '/usr/bin/pql', tmux: '/usr/bin/tmux', shell: '/bin/bash')); + f.services.toolchain.applyResolved(const ResolvedPaths(git: '/usr/bin/git', pql: '/usr/bin/pql', shell: '/bin/bash')); await tester.pumpWidget(harness(f, const ToolStatusItem())); await tester.pumpAndSettle(); expect(find.text('application ok'), findsOneWidget); }); testWidgets('missing tools render a warning chip per missing tool', (tester) async { - // git + tmux missing, pql resolved. - f.services.toolchain.applyResolved(const ResolvedPaths(pql: '/usr/bin/pql', shell: '/bin/bash')); + // git + pql missing, shell resolved → one chip each, nothing else. + f.services.toolchain.applyResolved(const ResolvedPaths(shell: '/bin/bash')); await tester.pumpWidget(harness(f, const ToolStatusItem())); await tester.pumpAndSettle(); expect(find.text('git not found'), findsOneWidget); - expect(find.text('tmux not found'), findsOneWidget); - expect(find.text('pql not found'), findsNothing); + expect(find.text('pql not found'), findsOneWidget); + expect(find.textContaining('not found'), findsNWidgets(2)); }); testWidgets('StatusItemContribution.build returns a ToolStatusItem', (tester) async { diff --git a/test/builtin/welcome/widget_test.dart b/test/builtin/welcome/widget_test.dart index 6f0cf0f0..3dc81134 100644 --- a/test/builtin/welcome/widget_test.dart +++ b/test/builtin/welcome/widget_test.dart @@ -79,7 +79,7 @@ void main() { }); testWidgets('status line shows "application ok" when all tools resolved', (tester) async { - f.services.toolchain.applyResolved(const ResolvedPaths(git: '/usr/bin/git', pql: '/usr/bin/pql', tmux: '/usr/bin/tmux', shell: '/bin/bash')); + f.services.toolchain.applyResolved(const ResolvedPaths(git: '/usr/bin/git', pql: '/usr/bin/pql', shell: '/bin/bash')); await tester.pumpWidget(harness(f, const WelcomeView())); await tester.pumpAndSettle(); expect(find.text('application ok'), findsOneWidget); @@ -90,11 +90,11 @@ void main() { tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); - f.services.toolchain.applyResolved(const ResolvedPaths(pql: '/usr/bin/pql')); + f.services.toolchain.applyResolved(const ResolvedPaths()); await tester.pumpWidget(harness(f, const WelcomeView())); await tester.pumpAndSettle(); expect(find.textContaining('git not found'), findsOneWidget); - expect(find.textContaining('tmux not found'), findsOneWidget); + expect(find.textContaining('pql not found'), findsOneWidget); }); testWidgets('theme-name link fires the theme.pick command when tapped', (tester) async { diff --git a/test/kernel/src/toolchain_paths_test.dart b/test/kernel/src/toolchain_paths_test.dart index 63cb9cd3..cb109ad4 100644 --- a/test/kernel/src/toolchain_paths_test.dart +++ b/test/kernel/src/toolchain_paths_test.dart @@ -10,11 +10,10 @@ void main() { group('ToolchainView.resolved', () { test('exposes the supplied paths verbatim', () { final v = ToolchainView.resolved( - const ResolvedPaths(git: '/opt/git', pql: '/opt/pql', tmux: '/opt/tmux', shell: '/usr/bin/zsh', gitEnv: {'GIT_EXEC_PATH': '/opt/git-core'}), + const ResolvedPaths(git: '/opt/git', pql: '/opt/pql', shell: '/usr/bin/zsh', gitEnv: {'GIT_EXEC_PATH': '/opt/git-core'}), ); expect(v.git, '/opt/git'); expect(v.pql, '/opt/pql'); - expect(v.tmux, '/opt/tmux'); expect(v.shell, '/usr/bin/zsh'); expect(v.gitEnv, {'GIT_EXEC_PATH': '/opt/git-core'}); expect(v.resolved, isTrue); @@ -26,22 +25,21 @@ void main() { final v = ToolchainView.resolved(const ResolvedPaths()); expect(v.git, 'git'); expect(v.pql, 'pql'); - expect(v.tmux, 'tmux'); expect(v.shell, '/bin/bash'); expect(v.gitEnv, isNull); expect(v.resolved, isTrue); expect(v.allOk, isFalse); - expect(v.missing, ['git', 'pql', 'tmux']); + expect(v.missing, ['git', 'pql']); }); test('missing reports only the unresolved tools', () { final v = ToolchainView.resolved( const ResolvedPaths( git: '/opt/git', - // pql + tmux null → missing. + // pql null → missing. ), ); - expect(v.missing, ['pql', 'tmux']); + expect(v.missing, ['pql']); expect(v.allOk, isFalse); }); }); diff --git a/test/kernel/src/toolchain_test.dart b/test/kernel/src/toolchain_test.dart index e9d03871..c9df9be2 100644 --- a/test/kernel/src/toolchain_test.dart +++ b/test/kernel/src/toolchain_test.dart @@ -13,26 +13,17 @@ void main() { final t = Toolchain(); expect(t.git, 'git'); expect(t.pql, 'pql'); - expect(t.tmux, 'tmux'); expect(t.shell, '/bin/bash'); expect(t.resolved, isFalse); expect(t.allOk, isFalse); - expect(t.missing, ['git', 'pql', 'tmux']); + expect(t.missing, ['git', 'pql']); }); test('applyResolved with full paths flips resolved + allOk + clears missing', () { final t = Toolchain(); var calls = 0; t.addListener(() => calls++); - t.applyResolved( - const ResolvedPaths( - git: '/usr/bin/git', - pql: '/usr/bin/pql', - tmux: '/usr/bin/tmux', - shell: '/bin/bash', - gitEnv: {'GIT_EXEC_PATH': '/usr/lib/git-core'}, - ), - ); + t.applyResolved(const ResolvedPaths(git: '/usr/bin/git', pql: '/usr/bin/pql', shell: '/bin/bash', gitEnv: {'GIT_EXEC_PATH': '/usr/lib/git-core'})); expect(t.resolved, isTrue); expect(t.allOk, isTrue); expect(t.missing, isEmpty); @@ -46,7 +37,7 @@ void main() { t.applyResolved(const ResolvedPaths(pql: '/usr/bin/pql')); expect(t.resolved, isTrue); expect(t.allOk, isFalse); - expect(t.missing, ['git', 'tmux']); + expect(t.missing, ['git']); }); test('waitForResolution completes immediately when already resolved', () async {