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;
+9
View File
@@ -198,4 +198,13 @@ Core, rendering, IPC, kernel, panel manager.
- **Cross-reference:** [D-047](#d-047-interaction-model-claude-is-home-layout), [D-048](#d-048-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first), [D-052](#d-052-focus-mode-full-window-takeover).
- **Raised by:** 2026-04-22 interaction model spec (Wireframe — Flows v3).
### D-055: Claude pane internal tabs for multi-session
- **Date:** 2026-04-23
- **Decision:** Multiple Claude sessions share the workspace as internal tabs inside the Claude pane header — not as workspace-level tabs (which would violate [D-048](#d-048-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first)). The primary session tab has no close affordance (per [D-041](#d-041-claude-panes-one-primary-per-repo-tmux-backed)). Secondary session tabs show a close `×`. A small `+` button sits at the right end of the tab row to spawn a new secondary. Double-clicking empty space in the tab row also spawns a new secondary. When a secondary is closed, focus collapses to the most-recently-active remaining tab (primary or another secondary). The tab row is hidden when only the primary exists — it appears on first secondary spawn and disappears when the last secondary closes. Session names in the tab row use the tmux session name slug (readable path, per the session naming convention).
- **Amendment to [D-041](#d-041-claude-panes-one-primary-per-repo-tmux-backed):** D-041 defined the lifecycle (primary persists, secondaries are ephemeral, close semantics) but left the multi-session UI unspecified. This record fills that gap. The `claude.new-secondary` command (already registered but not wired) is the spawn mechanism; the tab row is the UI surface.
- **Rationale:** The workspace is Claude's space ([D-047](#d-047-interaction-model-claude-is-home-layout)). Multiple Claude sessions are a Claude concern, not a workspace concern. Internal tabs keep the multiplicity contained — the workspace slot doesn't know how many sessions exist, it just renders the Claude pane. The hide-when-one rule keeps the common case (single primary) chrome-free.
- **Cost:** The Claude pane grows its own tab model (lightweight — just a list of session IDs + which is active). The `builtin.claude` extension owns this; no kernel changes needed.
- **Cross-reference:** [D-041](#d-041-claude-panes-one-primary-per-repo-tmux-backed), [D-047](#d-047-interaction-model-claude-is-home-layout), [D-048](#d-048-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first).
- **Raised by:** 2026-04-23 interaction model refinement.
---