wire the Claude pane onto the session orchestrator (T-169)
The pane no longer spawns/owns its StreamJsonSession — it spawns-or-binds through the app-wide ClaudeSessionOrchestrator by a stable pane key, and the orchestrator owns the session + conversation. Consequences: disposing a pane no longer kills its session (a kept-alive/hidden pane keeps it); the primary re-binds to its live session on remount (conversation survives); closing a secondary tab closes that session; /clear and /resume close + respawn through the orchestrator. The extension owns the orchestrator (set on activate, disposed on deactivate). Remaining for T-169: re-point TeamObserver from tmux-polling to orchestrating managed sessions, and roster-driven show/hide. T-169. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import 'conversation_view.dart';
|
||||
import 'prompt_card.dart';
|
||||
import 'session_index.dart';
|
||||
import 'session_naming.dart';
|
||||
import 'session_orchestrator.dart';
|
||||
import 'session_picker.dart';
|
||||
import 'slash_commands.dart';
|
||||
import 'stream_json_session.dart';
|
||||
@@ -64,6 +65,9 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
|
||||
bool _spawned = false;
|
||||
|
||||
/// This pane's stable key in the session orchestrator (T-169).
|
||||
String get _orchId => widget.isPrimary ? 'primary' : 'secondary-${widget.secondaryIndex}';
|
||||
|
||||
// The status line surfaced to the bottom status bar via ClidePane — the
|
||||
// live session fields (model/mode/context, T-150) plus the configured
|
||||
// skills count from ClaudeConfig (T-154). Null when there's nothing yet.
|
||||
@@ -109,8 +113,12 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
activeClaudeConfig?.removeListener(_onConfigChanged);
|
||||
_statusSub?.cancel();
|
||||
_statusSub = null;
|
||||
// The controller's onDispose kills the session (process + streams).
|
||||
_conversation?.dispose();
|
||||
// The orchestrator owns the session, so disposing this pane does NOT kill
|
||||
// it — that's what lets a hidden/kept-alive pane keep its session (T-169).
|
||||
// A secondary tab being *closed* is a real teardown, so close its session;
|
||||
// the primary persists (its deterministic id resumes next launch), and the
|
||||
// orchestrator disposes everything on extension teardown.
|
||||
if (!widget.isPrimary) unawaited(activeSessionOrchestrator?.close(_orchId));
|
||||
_conversation = null;
|
||||
_session = null;
|
||||
super.dispose();
|
||||
@@ -160,24 +168,32 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
final home = Platform.environment['HOME'] ?? '';
|
||||
final transcriptFile = '$home/.claude/projects/${repoRoot.replaceAll('/', '-')}/$_sessionId.jsonl';
|
||||
final resume = await File(transcriptFile).exists();
|
||||
final sessionArgs = claudeLaunchArgs(_sessionId!, resume: resume);
|
||||
|
||||
final StreamJsonSession session;
|
||||
// The orchestrator owns the session (T-169): spawn-or-bind by our pane key,
|
||||
// so the session (and its accumulating conversation) outlives this pane.
|
||||
final orch = activeSessionOrchestrator;
|
||||
if (orch == null) {
|
||||
setState(() => _error = 'Session orchestrator unavailable.');
|
||||
return;
|
||||
}
|
||||
final ManagedSession managed;
|
||||
try {
|
||||
final proc = await ClaudeStreamJsonProcess.start(sessionArgs: sessionArgs, cwd: repoRoot);
|
||||
session = StreamJsonSession(proc)..start();
|
||||
managed = await orch.spawn(SpawnSpec(
|
||||
id: _orchId,
|
||||
role: widget.isPrimary ? 'primary' : 'session ${widget.secondaryIndex}',
|
||||
sessionId: _sessionId!,
|
||||
cwd: repoRoot,
|
||||
resume: resume,
|
||||
));
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _error = 'Could not start claude: $e');
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
await session.dispose();
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
_session = session;
|
||||
_conversation = ConversationController(stream: session.items, onDispose: session.dispose);
|
||||
_statusSub = session.statusStream.listen((s) {
|
||||
_session = managed.session;
|
||||
_conversation = managed.conversation;
|
||||
_statusSub = managed.session.statusStream.listen((s) {
|
||||
if (!mounted) return;
|
||||
setState(() => _status = s);
|
||||
});
|
||||
@@ -229,11 +245,11 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
}
|
||||
|
||||
/// Tear the current session down and respawn bound to [sessionId]. The old
|
||||
/// process is killed; its transcript stays on disk (history preserved).
|
||||
/// process is killed via the orchestrator; its transcript stays on disk.
|
||||
Future<void> _respawnWithSession(String sessionId) async {
|
||||
_statusSub?.cancel();
|
||||
_statusSub = null;
|
||||
_conversation?.dispose(); // onDispose kills the old session
|
||||
await activeSessionOrchestrator?.close(_orchId); // kills the old session
|
||||
_conversation = null;
|
||||
_session = null;
|
||||
_sessionId = sessionId;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:clide/clide.dart';
|
||||
import 'package:clide/builtin/claude/src/claude_config.dart';
|
||||
import 'package:clide/builtin/claude/src/claude_session_host.dart';
|
||||
import 'package:clide/builtin/claude/src/session_naming.dart';
|
||||
import 'package:clide/builtin/claude/src/session_orchestrator.dart';
|
||||
import 'package:clide/builtin/claude/src/pane_context_status.dart';
|
||||
import 'package:clide/builtin/claude/src/claude_meta_sidebar.dart';
|
||||
import 'package:clide/builtin/claude/src/session_index.dart';
|
||||
@@ -32,6 +33,7 @@ class ClaudeExtension extends ClideExtension {
|
||||
|
||||
TeamObserver? _observer;
|
||||
ClaudeConfig? _config;
|
||||
ClaudeSessionOrchestrator? _orchestrator;
|
||||
final List<StreamSubscription<dynamic>> _subs = [];
|
||||
|
||||
/// App-wide Claude environment (skills, commands, settings, permissions,
|
||||
@@ -110,6 +112,11 @@ class ClaudeExtension extends ClideExtension {
|
||||
_subs.add(ctx.events.on<ProjectOpened>().listen((e) => cfg.setProjectDir(Directory(e.path))));
|
||||
}
|
||||
|
||||
// The clide-managed session set (T-169). Panes spawn/bind through it so a
|
||||
// session outlives its pane and is shared across surfaces.
|
||||
_orchestrator = ClaudeSessionOrchestrator();
|
||||
activeSessionOrchestrator = _orchestrator;
|
||||
|
||||
// Cold-start reap: kill any leftover secondary tmux sessions from
|
||||
// a previous run. D-41's "secondary numbering resets between
|
||||
// clide runs" only holds if the leftovers are gone before the new
|
||||
@@ -151,6 +158,9 @@ class ClaudeExtension extends ClideExtension {
|
||||
}
|
||||
_subs.clear();
|
||||
_stopObserver();
|
||||
if (identical(activeSessionOrchestrator, _orchestrator)) activeSessionOrchestrator = null;
|
||||
_orchestrator?.dispose();
|
||||
_orchestrator = null;
|
||||
if (identical(activeClaudeConfig, _config)) activeClaudeConfig = null;
|
||||
_config?.dispose();
|
||||
_config = null;
|
||||
|
||||
@@ -74,6 +74,11 @@ class ManagedSession {
|
||||
bool visible;
|
||||
}
|
||||
|
||||
/// App-wide orchestrator, set by the Claude extension on activate (like
|
||||
/// `activeClaudeConfig`). Panes spawn/bind their session through it so the
|
||||
/// session set is shared across panes, the cockpit, and team tiles.
|
||||
ClaudeSessionOrchestrator? activeSessionOrchestrator;
|
||||
|
||||
class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||
ClaudeSessionOrchestrator({ProcessFactory? processFactory}) : _factory = processFactory ?? _spawnClaude;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user