add D-055 Claude internal tabs, readable session name slugs

D-055: multiple Claude sessions share the workspace as internal
tabs inside the Claude pane header. Primary has no close; secondaries
show ×. Tab row hidden when only primary exists. + button and
double-click-empty spawn new secondaries.

Session names now use a readable path slug (strip $HOME, replace /
with -) with fallback to FNV-1a hash when the slug exceeds 80 chars.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 00:13:24 +02:00
co-authored by Claude Opus 4.6
parent 3f0d4e2c02
commit a526c5b9b7
2 changed files with 40 additions and 10 deletions
+31 -10
View File
@@ -1,15 +1,18 @@
/// Derive deterministic tmux session names for Claude panes (D-041).
///
/// The primary session name for a repo is `clide-claude-<hash>` where
/// `<hash>` is an 8-char FNV-1a-ish hex hash of the canonical
/// (absolute, symlink-resolved) repo path. Not cryptographic — we just
/// need short, stable, collision-resistant-enough strings. Secondary
/// sessions append `-N` for monotonically increasing `N`.
/// The primary session name encodes the repo path in a human-readable
/// form: `clide-claude-<path-slug>`. For example:
/// ~/projects/clide → clide-claude-projects-clide
/// /var/mnt/data/myapp → clide-claude-var-mnt-data-myapp
///
/// Secondary sessions append `-N`.
library;
import 'dart:io' show Platform;
/// Stable session name for the primary Claude pane of [repoRoot].
String primarySessionName(String repoRoot) {
return 'clide-claude-${_hash(repoRoot)}';
return 'clide-claude-${_slugify(repoRoot)}';
}
/// Nth secondary session name. [n] starts at 1.
@@ -17,11 +20,29 @@ String secondarySessionName(String repoRoot, int n) {
return '${primarySessionName(repoRoot)}-$n';
}
/// 8-char hex hash. FNV-1a over UTF-16 code units; independent of
/// platform endianness. Collision rate at N=1000 repos is still
/// vanishingly small (≈0.0001%).
// tmux session names max out at 256 chars; keep ours well under.
const _maxSlugLen = 80;
String _slugify(String path) {
final home = Platform.environment['HOME'] ?? '';
var p = path;
if (home.isNotEmpty && p.startsWith(home)) {
p = p.substring(home.length);
}
p = p.replaceAll('/', '-').replaceAll('.', '');
while (p.startsWith('-')) {
p = p.substring(1);
}
while (p.endsWith('-')) {
p = p.substring(0, p.length - 1);
}
if (p.isEmpty) p = 'root';
if (p.length > _maxSlugLen) return _hash(path);
return p;
}
String _hash(String s) {
var h = 2166136261; // FNV offset basis (32-bit)
var h = 2166136261;
for (var i = 0; i < s.length; i++) {
h ^= s.codeUnitAt(i);
h = (h * 16777619) & 0xffffffff;