handle /clear in clide: reset to a fresh session instead of forwarding

Claude Code's /clear forks the conversation to a new session-id, which
clide's transcript reader (pinned to the spawn --session-id) can't
follow — so after /clear the pane froze on the old transcript and looked
dead. clide now owns /clear: it's intercepted in the composer's send
path (never forwarded to tmux), tears the pane's session down, and
respawns a fresh empty one. A new session-id is forced even for the
primary so it starts empty rather than resuming the old transcript;
_spawn's self-heal kills the stale tmux session. The old transcript is
left on disk.

Known follow-up (T-156): /resume and /compact have the same forking
problem but need different handling (a session picker, not a wipe).

T-156.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 13:36:01 +02:00
co-authored by Claude Opus 4.7
parent f6a5d72ccd
commit c4cc68ad2e
6 changed files with 79 additions and 0 deletions
+32
View File
@@ -301,6 +301,13 @@ class _ClaudePaneState extends State<ClaudePane> {
// The no-tmux fallback runs claude directly in our PTY, where pane.write
// does reach it.
void _send(String text) {
// Commands clide owns (T-156) are handled here, never forwarded — Claude
// Code's /clear forks the session to a new id our reader can't follow, so
// we tear this session down and start a fresh one instead.
if (clideOwnedCommand(text) == 'clear') {
unawaited(_clearSession());
return;
}
if (_usingTmux) {
final session = _sessionName;
if (session == null) return;
@@ -321,6 +328,31 @@ class _ClaudePaneState extends State<ClaudePane> {
unawaited(ipc.request('pane.write', args: {'id': id, 'text': encodeClaudeInput(text)}));
}
/// clide-owned `/clear` (T-156): tear this pane's session down and respawn a
/// brand-new, empty one. A fresh session id is forced — even for the primary,
/// whose id is normally deterministic — so we start empty rather than resume
/// the old transcript; _spawn's self-heal kills the now-stale tmux session
/// because the new id has no transcript yet. The old transcript is left on
/// disk (history preserved, just detached from this pane).
Future<void> _clearSession() async {
_conversation?.dispose();
_conversation = null;
unawaited(_feed?.dispose());
_feed = null;
_statusSub?.cancel();
_statusSub = null;
_eventSub?.cancel();
_eventSub = null;
_sessionId = freshSessionId();
if (mounted) {
setState(() {
_status = const SessionStatus();
_statusLine = 'clearing…';
});
}
await _spawn();
}
void _subscribe() {
final kernel = _kernel();
if (kernel == null) return;
@@ -27,6 +27,18 @@ bool isKnownSlashCommand(String text, Iterable<String> known) {
return token != null && known.contains(token);
}
/// Slash commands clide handles itself instead of forwarding to Claude:
/// Claude Code's own handling forks the session to a new id that clide's
/// transcript reader can't follow, so clide owns the semantics (T-156).
const Set<String> kClideOwnedCommands = {'clear'};
/// The clide-owned command in [text] (a single-line leading-slash token in
/// [kClideOwnedCommands]), or null.
String? clideOwnedCommand(String text) {
final token = slashCommandToken(text);
return token != null && kClideOwnedCommands.contains(token) ? token : null;
}
bool _isWs(String c) => c == ' ' || c == '\t' || c == '\n';
/// An in-progress slash query at the cursor — the `/` position and the word