add a native startup banner to the empty Claude pane (T-149)

Replaces the bare "Waiting for Claude…" empty state with ClaudeBanner:
clide's own logo, a "Claude" label, the session role (primary /
secondary N), the workspace (home-collapsed), the tmux status line, and
a warming-up hint. ConversationView gains an optional emptyState widget;
the pane supplies the banner from data it already has.

Fully owned — no tmux capture-pane, no Anthropic artwork. The "Claude"
label uses Anthropic's published accent #d97757 nominatively; recorded
under a new trademark_notices section in assets/licenses.yaml (clide is
unaffiliated, bundles no Anthropic logo/artwork). Also fixes a stale
forkpty->pty reference in that file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 09:26:07 +02:00
co-authored by Claude Opus 4.7
parent b52283b7c0
commit af483c2c34
8 changed files with 147 additions and 5 deletions
+65
View File
@@ -0,0 +1,65 @@
/// Native startup banner for the empty Claude pane (T-149).
///
/// Shown by [ConversationView] before any transcript items arrive — a
/// clide-branded placeholder (clide's own logo) that orients the user:
/// which session, which workspace, what status. The "Claude" label uses
/// Anthropic's published accent colour ([claudeAccent], #d97757) purely
/// to identify Claude — nominative use; clide bundles no Anthropic
/// artwork (see assets/licenses.yaml trademark_notices).
library;
import 'dart:io' show Platform;
import 'package:clide/builtin/claude/src/conversation_view.dart' show claudeAccent;
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
class ClaudeBanner extends StatelessWidget {
const ClaudeBanner({super.key, required this.role, this.workspace, this.statusLine});
/// Session role, e.g. `'primary'` or `'secondary 2'`.
final String role;
/// Repo root for this session (shown home-collapsed).
final String? workspace;
/// Status line, e.g. `'tmux · clide-claude-…'`.
final String? statusLine;
@override
Widget build(BuildContext context) {
final ws = _collapseHome(workspace);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ClideSvgView.asset('assets/logo/logo.svg', width: 60, height: 60),
const SizedBox(height: 18),
ClideText(
'Claude',
fontSize: clideFontDialogTitle,
color: claudeAccent,
fontWeight: FontWeight.w500,
),
const SizedBox(height: 2),
ClideText(role, fontSize: clideFontSmall, muted: true, fontFamily: clideMonoFamily),
const SizedBox(height: 16),
if (ws != null) ClideText(ws, fontSize: clideFontCaption, muted: true),
if (statusLine != null) ...[
const SizedBox(height: 2),
ClideText(statusLine!, fontSize: clideFontSmall, muted: true, fontFamily: clideMonoFamily),
],
const SizedBox(height: 16),
const ClideText('Warming up — your conversation will appear here.', fontSize: clideFontSmall, muted: true),
],
),
);
}
static String? _collapseHome(String? path) {
if (path == null) return null;
final home = Platform.environment['HOME'] ?? '';
if (home.isNotEmpty && path.startsWith(home)) return '~${path.substring(home.length)}';
return path;
}
}
+13 -1
View File
@@ -7,6 +7,7 @@ import 'package:clide/widgets/widgets.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/widgets.dart';
import 'claude_banner.dart';
import 'claude_composer.dart';
import 'clipboard_paste.dart';
import 'conversation_controller.dart';
@@ -46,6 +47,7 @@ class _ClaudePaneState extends State<ClaudePane> {
String? _paneId;
String? _sessionName;
String? _sessionId;
String? _repoRoot;
String? _error;
String _statusLine = 'attaching…';
@@ -142,6 +144,7 @@ class _ClaudePaneState extends State<ClaudePane> {
if (rootResp.ok) {
repoRoot = (rootResp.data['path'] as String?) ?? repoRoot;
}
_repoRoot = repoRoot;
_sessionName = widget.isPrimary ? primarySessionName(repoRoot) : secondarySessionName(repoRoot, widget.secondaryIndex!);
// Bind this pane to a specific Claude session id so concurrent
@@ -310,7 +313,16 @@ class _ClaudePaneState extends State<ClaudePane> {
} else if (_conversation != null) {
body = Column(
children: [
Expanded(child: ConversationView(controller: _conversation!)),
Expanded(
child: ConversationView(
controller: _conversation!,
emptyState: ClaudeBanner(
role: widget.isPrimary ? 'primary' : 'secondary ${widget.secondaryIndex}',
workspace: _repoRoot,
statusLine: _statusLine,
),
),
),
ClaudeComposer(
enabled: _paneId != null,
onSubmit: _send,
@@ -23,6 +23,7 @@ class ConversationView extends StatefulWidget {
super.key,
required this.controller,
this.wrapInSelectionArea = true,
this.emptyState,
});
final ConversationController controller;
@@ -32,6 +33,10 @@ class ConversationView extends StatefulWidget {
/// selection spans tiles — nesting SelectionAreas is illegal (T-140).
final bool wrapInSelectionArea;
/// Shown while there are no items yet (e.g. the [ClaudeBanner] startup
/// banner). Defaults to a plain "Waiting for Claude…" hint.
final Widget? emptyState;
@override
State<ConversationView> createState() => _ConversationViewState();
}
@@ -80,9 +85,7 @@ class _ConversationViewState extends State<ConversationView> {
if (items.isEmpty) {
return ColoredBox(
color: tokens.panelBackground,
child: const Center(
child: ClideText('Waiting for Claude…', muted: true),
),
child: widget.emptyState ?? const Center(child: ClideText('Waiting for Claude…', muted: true)),
);
}