move the per-session status to the bottom status bar (T-145)
Per feedback, the model · permission-mode · context line reads better in the status bar than as a strip above the conversation. Adds a generic, publisher-agnostic status-bar context slot: a pane publishes a short string to the `statusbar.context` MessageBus channel and the bar shows the latest. The active Claude sub-tab publishes (inactive panes stay quiet, so no race); switching tabs swaps the slot to the focused pane. Replaces the in-pane ClaudeStatusStrip with a formatStatusLine helper + PaneContextStatusItem (the status-bar widget) and a StatusItemContribution. ClaudeSessionHost passes `active` so only the visible sub-tab publishes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,12 +9,13 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'claude_banner.dart';
|
||||
import 'claude_composer.dart';
|
||||
import 'claude_status_strip.dart';
|
||||
import 'claude_status.dart';
|
||||
import 'clipboard_paste.dart';
|
||||
import 'conversation_controller.dart';
|
||||
import 'conversation_view.dart';
|
||||
import 'session_naming.dart';
|
||||
import 'tmux_session.dart' as tmux;
|
||||
import 'pane_context_status.dart';
|
||||
import 'transcript_publisher.dart';
|
||||
import 'transcript_reader.dart';
|
||||
|
||||
@@ -24,12 +25,17 @@ class ClaudePane extends StatefulWidget {
|
||||
this.isPrimary = true,
|
||||
this.secondaryIndex,
|
||||
this.showChrome = true,
|
||||
this.active = true,
|
||||
}) : assert(isPrimary || secondaryIndex != null, 'secondary panes need an index');
|
||||
|
||||
final bool isPrimary;
|
||||
final bool showChrome;
|
||||
final int? secondaryIndex;
|
||||
|
||||
/// Whether this pane is the visible/focused tab. Only the active pane
|
||||
/// publishes its status to the status-bar context slot (T-145).
|
||||
final bool active;
|
||||
|
||||
@override
|
||||
State<ClaudePane> createState() => _ClaudePaneState();
|
||||
}
|
||||
@@ -57,6 +63,24 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
bool _spawned = false;
|
||||
bool _usingTmux = false;
|
||||
|
||||
// Publish this pane's status line to the status-bar context slot, but
|
||||
// only when it's the active tab — the active pane owns the slot; an
|
||||
// inactive pane staying quiet lets the active one win without a race
|
||||
// (T-145). Switching tabs re-publishes from the newly-active pane.
|
||||
void _publishContext() {
|
||||
if (!widget.active || _status.isEmpty) return;
|
||||
final messages = _kernel()?.messages;
|
||||
if (messages == null) return;
|
||||
publishPaneContext(messages, 'builtin.claude', formatStatusLine(_status));
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ClaudePane old) {
|
||||
super.didUpdateWidget(old);
|
||||
// Became the active tab → push our status into the slot.
|
||||
if (widget.active && !old.active) _publishContext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -256,7 +280,9 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
);
|
||||
_conversation = ConversationController.fromBus(messages: messages, channel: channel);
|
||||
_statusSub = _feed!.statusStream.listen((s) {
|
||||
if (mounted) setState(() => _status = s);
|
||||
if (!mounted) return;
|
||||
setState(() => _status = s);
|
||||
_publishContext();
|
||||
});
|
||||
_subscribe();
|
||||
setState(() {});
|
||||
@@ -329,7 +355,6 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
} else if (_conversation != null) {
|
||||
body = Column(
|
||||
children: [
|
||||
if (!_status.isEmpty) ClaudeStatusStrip(status: _status),
|
||||
Expanded(
|
||||
child: ConversationView(
|
||||
controller: _conversation!,
|
||||
|
||||
@@ -69,6 +69,8 @@ class ClaudeSessionHostState extends State<ClaudeSessionHost> {
|
||||
// The MultitabPane already provides the tab strip header;
|
||||
// suppressing the ClaudePane's own chrome avoids a double row.
|
||||
showChrome: false,
|
||||
// Only the visible sub-tab publishes to the status-bar slot.
|
||||
active: entry.id == _controller.activeId,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/// Formatting for the per-session status line (T-145): model, permission
|
||||
/// mode, and context-window token count, joined for the status-bar slot.
|
||||
///
|
||||
/// Context is a token *count*, not a percentage — the transcript carries
|
||||
/// `message.usage` but not the model's context limit, and the model id
|
||||
/// doesn't encode the 1M vs 200k tier, so a percentage would be guesswork.
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart';
|
||||
|
||||
/// Build the status-bar line, e.g. `opus 4.7 · default · 21k ctx`.
|
||||
/// Empty string when there's nothing to show.
|
||||
String formatStatusLine(SessionStatus status) {
|
||||
final parts = [
|
||||
if (status.model != null) shortModelLabel(status.model!),
|
||||
if (status.permissionMode != null) permissionModeLabel(status.permissionMode!),
|
||||
if (status.contextTokens != null) '${formatTokenCount(status.contextTokens!)} ctx',
|
||||
];
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
/// `claude-opus-4-7` → `opus 4.7`; unknown shapes pass through.
|
||||
String shortModelLabel(String model) {
|
||||
final s = model.startsWith('claude-') ? model.substring('claude-'.length) : model;
|
||||
final parts = s.split('-');
|
||||
if (parts.length >= 2) return '${parts.first} ${parts.sublist(1).join('.')}';
|
||||
return s;
|
||||
}
|
||||
|
||||
/// Friendly label for Claude's permission modes.
|
||||
String permissionModeLabel(String mode) {
|
||||
switch (mode) {
|
||||
case 'acceptEdits':
|
||||
return 'accept-edits';
|
||||
case 'bypassPermissions':
|
||||
return 'bypass';
|
||||
case 'plan':
|
||||
return 'plan';
|
||||
case 'default':
|
||||
return 'default';
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact token count: `765k`, `1.2M`, or the raw number under 1k.
|
||||
String formatTokenCount(int n) {
|
||||
if (n >= 1000000) return '${(n / 1000000).toStringAsFixed(1)}M';
|
||||
if (n >= 1000) return '${(n / 1000).round()}k';
|
||||
return '$n';
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/// A thin per-session status strip (T-145): current model, permission
|
||||
/// mode, and context-window token count, shown above the conversation.
|
||||
///
|
||||
/// Context is a token *count*, not a percentage — the transcript carries
|
||||
/// `message.usage` but not the model's context limit, and the model id
|
||||
/// doesn't encode the 1M vs 200k tier, so a percentage would be guesswork.
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart';
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ClaudeStatusStrip extends StatelessWidget {
|
||||
const ClaudeStatusStrip({super.key, required this.status});
|
||||
|
||||
final SessionStatus status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final parts = [
|
||||
if (status.model != null) shortModelLabel(status.model!),
|
||||
if (status.permissionMode != null) permissionModeLabel(status.permissionMode!),
|
||||
if (status.contextTokens != null) '${formatTokenCount(status.contextTokens!)} ctx',
|
||||
];
|
||||
if (parts.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: tokens.panelBorder)),
|
||||
),
|
||||
child: ClideText(
|
||||
parts.join(' · '),
|
||||
fontSize: clideFontSmall,
|
||||
muted: true,
|
||||
fontFamily: clideMonoFamily,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `claude-opus-4-7` → `opus 4.7`; unknown shapes pass through.
|
||||
String shortModelLabel(String model) {
|
||||
final s = model.startsWith('claude-') ? model.substring('claude-'.length) : model;
|
||||
final parts = s.split('-');
|
||||
if (parts.length >= 2) return '${parts.first} ${parts.sublist(1).join('.')}';
|
||||
return s;
|
||||
}
|
||||
|
||||
/// Friendly label for Claude's permission modes.
|
||||
String permissionModeLabel(String mode) {
|
||||
switch (mode) {
|
||||
case 'acceptEdits':
|
||||
return 'accept-edits';
|
||||
case 'bypassPermissions':
|
||||
return 'bypass';
|
||||
case 'plan':
|
||||
return 'plan';
|
||||
case 'default':
|
||||
return 'default';
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact token count: `765k`, `1.2M`, or the raw number under 1k.
|
||||
String formatTokenCount(int n) {
|
||||
if (n >= 1000000) return '${(n / 1000000).toStringAsFixed(1)}M';
|
||||
if (n >= 1000) return '${(n / 1000).round()}k';
|
||||
return '$n';
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:clide/clide.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/pane_context_status.dart';
|
||||
import 'package:clide/builtin/claude/src/team_observer.dart';
|
||||
import 'package:clide/builtin/claude/src/team_panel_host.dart';
|
||||
import 'package:clide/builtin/claude/src/tmux_session.dart' as tmux;
|
||||
@@ -52,6 +53,13 @@ class ClaudeExtension extends ClideExtension {
|
||||
title: 'Claude: kill all tmux sessions for this repo',
|
||||
run: _killAllSessions,
|
||||
),
|
||||
// In-pane status slot (T-145): the active Claude pane publishes
|
||||
// its model · permission-mode · context line here.
|
||||
StatusItemContribution(
|
||||
id: 'claude.status-context',
|
||||
priority: 50,
|
||||
build: (_) => const PaneContextStatusItem(),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/// The status-bar "in-pane context" slot (T-145).
|
||||
///
|
||||
/// A generic, publisher-agnostic slot: a pane publishes a short status
|
||||
/// string to [paneContextChannel] on the MessageBus, and the bottom
|
||||
/// status bar shows the latest one. The active pane publishes (an
|
||||
/// inactive pane stays quiet), so switching tabs swaps the slot to the
|
||||
/// newly-active pane's message. The Claude pane is the first publisher
|
||||
/// (model · permission-mode · context); other panes can use the same
|
||||
/// channel.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// MessageBus channel for the status-bar context slot. Data: `{'text': String}`.
|
||||
const paneContextChannel = 'statusbar.context';
|
||||
|
||||
/// Publish [text] to the context slot (empty string clears it).
|
||||
void publishPaneContext(MessageBus messages, String publisher, String text) {
|
||||
messages.publish(publisher, paneContextChannel, {'text': text});
|
||||
}
|
||||
|
||||
/// Status-bar item that shows the latest pane-context message (nothing
|
||||
/// when empty). Subscribes to the bus itself via the ambient kernel.
|
||||
class PaneContextStatusItem extends StatefulWidget {
|
||||
const PaneContextStatusItem({super.key});
|
||||
|
||||
@override
|
||||
State<PaneContextStatusItem> createState() => _PaneContextStatusItemState();
|
||||
}
|
||||
|
||||
class _PaneContextStatusItemState extends State<PaneContextStatusItem> {
|
||||
StreamSubscription<Message>? _sub;
|
||||
String _text = '';
|
||||
bool _subscribed = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_subscribed) return;
|
||||
_subscribed = true;
|
||||
final messages = ClideKernel.of(context).messages;
|
||||
_sub = messages.subscribe(channel: paneContextChannel).listen((m) {
|
||||
final t = (m.data['text'] as String?) ?? '';
|
||||
if (t == _text || !mounted) return;
|
||||
setState(() => _text = t);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_text.isEmpty) return const SizedBox.shrink();
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: ClideText(
|
||||
_text,
|
||||
fontSize: clideFontSmall,
|
||||
fontFamily: clideMonoFamily,
|
||||
color: tokens.statusBarForeground,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user