primary /clear empties the deterministic session in place (T-268)
The primary Claude pane is anchored to a deterministic session id derived from the repo path so it resumes the same transcript across restarts (D-77/T-146). But /clear (T-156) respawned on a fresh RANDOM id, so the next launch re-resolved to the deterministic id, found its old transcript on disk, and resumed the PRE-clear conversation — the cleared session was orphaned and the clear silently didn't stick. /clear in the primary pane now deletes the deterministic session's transcript (and its sidecar dir) and respawns on the SAME id, so `--session-id` re-creates it empty and a cleared primary stays cleared. Secondary panes are throwaway and keep the fresh-random behaviour. Factor the duplicated transcript-path construction out of claude_pane into session_naming helpers (claudeProjectDir / claudeTranscriptPath / clearSessionTranscript) so the clear logic is DRY and unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -236,8 +236,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
|
||||
// A transcript already on disk means the session existed before, so resume
|
||||
// it; `claude --session-id <id>` refuses an existing id (T-161/D-77).
|
||||
final home = Platform.environment['HOME'] ?? '';
|
||||
final transcriptFile = '$home/.claude/projects/${repoRoot.replaceAll('/', '-')}/$_sessionId.jsonl';
|
||||
final transcriptFile = claudeTranscriptPath(repoRoot, _sessionId!);
|
||||
final resume = await File(transcriptFile).exists();
|
||||
|
||||
try {
|
||||
@@ -338,9 +337,23 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
widget.onFork?.call(sourceId);
|
||||
}
|
||||
|
||||
/// clide-owned `/clear` (T-156): respawn on a brand-new, empty session.
|
||||
/// clide-owned `/clear` (T-156).
|
||||
///
|
||||
/// The primary pane is anchored to a deterministic session id that resumes
|
||||
/// across restarts (D-77/T-146), so clearing it must empty THAT session in
|
||||
/// place: delete its transcript and respawn fresh on the SAME id. Spawning a
|
||||
/// throwaway random id instead would orphan the cleared state — the next
|
||||
/// launch would re-resolve to the deterministic id and resume the pre-clear
|
||||
/// conversation, which is exactly the continuity break this fixes (T-268).
|
||||
/// Secondary panes are throwaway sessions, so for them a fresh random id is
|
||||
/// the clear.
|
||||
Future<void> _clearSession() async {
|
||||
if (mounted) setState(() => _statusLine = 'clearing…');
|
||||
final root = _repoRoot;
|
||||
if (widget.isPrimary && root != null) {
|
||||
await _respawnWithSession(primarySessionId(root), clearTranscript: true);
|
||||
return;
|
||||
}
|
||||
await _respawnWithSession(freshSessionId());
|
||||
}
|
||||
|
||||
@@ -350,8 +363,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
final root = _repoRoot;
|
||||
final dialog = _kernel()?.dialog;
|
||||
if (root == null || dialog == null) return;
|
||||
final home = Platform.environment['HOME'] ?? '';
|
||||
final dir = Directory('$home/.claude/projects/${root.replaceAll('/', '-')}');
|
||||
final dir = Directory(claudeProjectDir(root));
|
||||
final sessions = await listSessions(dir);
|
||||
if (!mounted) return;
|
||||
final picked = await dialog.show<String>(
|
||||
@@ -367,11 +379,18 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
}
|
||||
|
||||
/// Tear the current session down and respawn bound to [sessionId]. The old
|
||||
/// process is killed via the orchestrator; its transcript stays on disk.
|
||||
Future<void> _respawnWithSession(String sessionId) async {
|
||||
/// process is killed via the orchestrator; its transcript stays on disk
|
||||
/// unless [clearTranscript] is set, in which case [sessionId]'s transcript is
|
||||
/// erased after the kill so `--session-id` re-creates it empty (T-268).
|
||||
Future<void> _respawnWithSession(String sessionId, {bool clearTranscript = false}) async {
|
||||
_statusSub?.cancel();
|
||||
_statusSub = null;
|
||||
await activeSessionOrchestrator?.close(_orchId); // kills the old session
|
||||
// Erase only after the process is dead, so claude isn't mid-write.
|
||||
final root = _repoRoot;
|
||||
if (clearTranscript && root != null) {
|
||||
await clearSessionTranscript(claudeProjectDir(root), sessionId);
|
||||
}
|
||||
_conversation = null;
|
||||
_session = null;
|
||||
_sessionId = sessionId;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:io' show Directory, File, Platform;
|
||||
import 'dart:math';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -82,6 +82,37 @@ List<String> claudeLaunchArgs(String sessionId, {required bool resume}) => resum
|
||||
/// `init` event.
|
||||
List<String> forkSessionArgs(String sourceSessionId) => ['--resume', sourceSessionId, '--fork-session'];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transcript locations on disk (T-161 / T-268)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The directory claude stores [repoRoot]'s transcripts in:
|
||||
/// `~/.claude/projects/<munged-repo-root>`, where the munge replaces `/` with
|
||||
/// `-`. Matches claude's own project-dir naming and is the single source of
|
||||
/// truth for the path that [ClaudePane] both probes (resume vs create) and
|
||||
/// lists (`/resume` picker).
|
||||
String claudeProjectDir(String repoRoot) {
|
||||
final home = Platform.environment['HOME'] ?? '';
|
||||
return '$home/.claude/projects/${repoRoot.replaceAll('/', '-')}';
|
||||
}
|
||||
|
||||
/// The transcript JSONL path for [sessionId] under [repoRoot]. Its existence is
|
||||
/// what decides `--resume` vs `--session-id` (T-161).
|
||||
String claudeTranscriptPath(String repoRoot, String sessionId) => '${claudeProjectDir(repoRoot)}/$sessionId.jsonl';
|
||||
|
||||
/// Erase [sessionId]'s transcript under [projectDir] so a subsequent
|
||||
/// `claude --session-id <sessionId>` re-creates it empty — the in-place
|
||||
/// `/clear` path for the primary pane (T-268). Removes both the `<id>.jsonl`
|
||||
/// and the sidecar `<id>/` directory claude keeps beside it. Best-effort:
|
||||
/// missing entries are not an error. The caller MUST have killed the session's
|
||||
/// process first, so claude is not mid-write.
|
||||
Future<void> clearSessionTranscript(String projectDir, String sessionId) async {
|
||||
final file = File('$projectDir/$sessionId.jsonl');
|
||||
if (await file.exists()) await file.delete();
|
||||
final dir = Directory('$projectDir/$sessionId');
|
||||
if (await dir.exists()) await dir.delete(recursive: true);
|
||||
}
|
||||
|
||||
/// A fresh random session id for a secondary pane — secondaries are
|
||||
/// always clean sessions, never resumed.
|
||||
String freshSessionId() {
|
||||
|
||||
Reference in New Issue
Block a user