remove tmux as a required tool (dead since D-77)

Nothing has spawned tmux since D-77 moved Claude session persistence to
`--resume`; Claude and terminal panes spawn `claude` or the shell directly.
But the toolchain still resolved tmux and listed it in `missing`, so on
mac/linux a box without tmux showed a spurious "tmux not found" warning in
the welcome view + status bar. The windows-support branch had special-cased
that away with a `!Platform.isWindows` guard — the tell that the requirement
was dead everywhere, not platform-specific.

Drop tmux from ResolvedPaths / ToolchainView / Toolchain (field, getter,
`missing`, PATH resolution) on every platform, removing the Windows guards
with it. Strip the testmode tmux probes and the comments / CLAUDE.md line
that claimed clide spawns tmux. (The dead ToolCheck class that also gated on
tmux was already deleted on main and dropped in the preceding merge.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 18:22:04 +02:00
co-authored by Claude Opus 4.8
parent 84a6ef7c77
commit 431e26d659
12 changed files with 29 additions and 60 deletions
+6
View File
@@ -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
+1 -1
View File
@@ -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 <session-id>` 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/).
+1 -11
View File
@@ -21,7 +21,6 @@ export 'toolchain_paths.dart';
class Toolchain extends ChangeNotifier implements ToolchainView {
String? _git;
String? _pql;
String? _tmux;
String? _shell;
Map<String, String>? _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<String> 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<String> get missing => [if (_git == null) 'git', if (_pql == null) 'pql'];
/// Returns a Future that completes when resolution finishes.
Future<void> 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;
+3 -13
View File
@@ -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<String, String>? gitEnv;
}
@@ -32,7 +31,6 @@ abstract class ToolchainView {
String get git;
String get pql;
String get tmux;
String get shell;
Map<String, String>? 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<String, String>? get gitEnv => _paths.gitEnv;
@@ -60,13 +56,7 @@ class _StaticToolchain implements ToolchainView {
@override
bool get allOk => missing.isEmpty;
@override
List<String> 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<String> 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
+1 -1
View File
@@ -41,7 +41,7 @@ const Map<String, String> 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',
};
+1 -1
View File
@@ -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;
-6
View File
@@ -113,22 +113,17 @@ class _ClideTestAppState extends State<ClideTestApp> {
_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<ClideTestApp> {
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('');
+1 -1
View File
@@ -16,7 +16,7 @@ typedef MultitabEntryCallback<T> = void Function(MultitabEntry<T> 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<T> extends StatelessWidget {
+5 -5
View File
@@ -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 {
+3 -3
View File
@@ -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 {
+4 -6
View File
@@ -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);
});
});
+3 -12
View File
@@ -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 {