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:
@@ -2194,3 +2194,5 @@ Acceptance: a secondary tab shows its OWN fresh conversation, independent of the
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-146', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 06:19:51', '2026-05-23 06:19:51', '2026-05-23 06:19:51', NULL, 'dd4ec6cfbbc3118cd4cf02604b91108b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-146', 'status', 'in_progress', 'done', NULL, '2026-05-23 06:30:47', '2026-05-23 06:30:47', '2026-05-23 06:30:47', NULL, '93f9c6814b9e82e5b60ac0e57fff5afe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 07:03:18', '2026-05-23 07:03:18', '2026-05-23 07:03:18', NULL, 'b0bc1949f98ef05a05717cef80ba194f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'status', 'in_progress', 'done', NULL, '2026-05-23 07:06:54', '2026-05-23 07:06:54', '2026-05-23 07:06:54', NULL, 'b6b88954fdc04ecf7847dea204a92e14', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-149', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 07:14:36', '2026-05-23 07:14:36', '2026-05-23 07:14:36', NULL, '75bb1fa2d964f284910201b1c15995b9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -2552,3 +2552,5 @@ Acceptance: a secondary tab shows its OWN fresh conversation, independent of the
|
||||
', 'done', 'high', NULL, NULL, NULL, '2026-05-23 06:19:51', '2026-05-23 06:30:47', NULL, '2e1843c9b540a3d7ced03052d9efcbc4', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'bug', 'T-132', 'Claude pane gets stuck waiting on a stale/legacy session', 'When the bound transcript (<munged-cwd>/<session-id>.jsonl) never appears — e.g. attaching via new-session -A to a session created before --session-id binding (T-146), or any unconnectable session — the pane shows ''Waiting for Claude…'' forever. Fix: before spawning, if the bound transcript for our deterministic session id does not exist, kill the clide tmux session of that exact name so new-session creates a clean session with our --session-id. SAFETY (hard requirement): only ever kill clide-initiated sessions — by exact clide-claude-<slug> name, on the private -L clide socket (a user''s terminal claude is never on that socket); never delete any transcript file. Also makes clean-install/first-run robust. Live-verify the stuck primary self-heals on relaunch and a secondary still gets a fresh independent session.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-23 07:03:18', '2026-05-23 07:03:18', NULL, '21f7ebec0ddcab48a3ea7e8be0c0e979', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-148', 'task', 'T-132', 'Claude transcript disk-usage indicator + cleanup button', 'Surface transcript/session disk usage in the Claude panel with a cleanup affordance so the user can pick and choose what to remove. Per-session sizes (the <munged-cwd>/*.jsonl files + subagents/), a total, and a button to delete selected sessions'' transcripts. User-driven only — clide never auto-deletes transcript content. Future/''at some point'' per user. Relates to T-146 (session ids) and D-75.', 'backlog', 'low', NULL, NULL, NULL, '2026-05-23 07:03:18', '2026-05-23 07:03:18', NULL, '7bbe7f4baf80027516cc46cff93a8dfe', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'bug', 'T-132', 'Claude pane gets stuck waiting on a stale/legacy session', 'When the bound transcript (<munged-cwd>/<session-id>.jsonl) never appears — e.g. attaching via new-session -A to a session created before --session-id binding (T-146), or any unconnectable session — the pane shows ''Waiting for Claude…'' forever. Fix: before spawning, if the bound transcript for our deterministic session id does not exist, kill the clide tmux session of that exact name so new-session creates a clean session with our --session-id. SAFETY (hard requirement): only ever kill clide-initiated sessions — by exact clide-claude-<slug> name, on the private -L clide socket (a user''s terminal claude is never on that socket); never delete any transcript file. Also makes clean-install/first-run robust. Live-verify the stuck primary self-heals on relaunch and a secondary still gets a fresh independent session.', 'done', 'high', NULL, NULL, NULL, '2026-05-23 07:03:18', '2026-05-23 07:06:54', NULL, '4c08227959a8fe6905521ebdc42487c1', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-149', 'task', 'T-132', 'Native Claude pane startup banner (empty state)', 'Replace the bland ''Waiting for Claude…'' empty state with a native clide banner: clide logo, a Claude label (brand orange), the session role (primary/secondary N), the workspace path (home-collapsed), and the tmux session/status line, plus a ''warming up'' hint. Owned/native — no tmux capture-pane, no CC-internals coupling. ConversationView gains an optional emptyState widget; ClaudePane supplies a ClaudeBanner built from data it already has (repoRoot, statusLine, isPrimary).', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-23 07:14:36', '2026-05-23 07:14:36', NULL, '741b021184df1596b364aa2d09167e85', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -91,6 +91,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Changed
|
||||
|
||||
- The empty Claude pane now shows a native startup banner — clide logo,
|
||||
session role, workspace, and tmux status — instead of a bare "Waiting
|
||||
for Claude…" (T-149).
|
||||
- User and Claude turns in the conversation now render as distinct
|
||||
accent-striped cards over a filled background — your prompts in the
|
||||
theme focus colour, Claude's replies in Claude's brand orange
|
||||
|
||||
+17
-1
@@ -226,4 +226,20 @@ dev_dependencies:
|
||||
license: BSD-3-Clause
|
||||
purpose: >-
|
||||
Dart test runner for the Flutter-free PTY tests under
|
||||
`dart test --tags forkpty` (flutter_test is used elsewhere).
|
||||
`dart test --tags pty` (flutter_test is used elsewhere).
|
||||
|
||||
# Trademark / brand notices — third-party marks clide references but does
|
||||
# NOT bundle. No artefact ships for these; tracked here for audit
|
||||
# completeness and to record the nominative-use basis.
|
||||
trademark_notices:
|
||||
- mark: Claude name + accent colour (#d97757)
|
||||
owner: Anthropic, PBC
|
||||
reference: https://github.com/anthropics/skills/tree/main/skills/brand-guidelines
|
||||
usage: >-
|
||||
Nominative use only. "Claude" identifies the CLI that clide
|
||||
integrates with; #d97757 is Anthropic's published Claude accent
|
||||
colour, used solely to mark Claude's own messages/labels in the
|
||||
conversation UI (not as clide's brand colour). clide is not
|
||||
affiliated with, endorsed by, or partnered with Anthropic, bundles
|
||||
no Anthropic logo or brand artwork, and ships its own logo. CLAUDE
|
||||
is a registered trademark of Anthropic, PBC.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/builtin/claude/src/claude_banner.dart';
|
||||
import 'package:clide/builtin/claude/src/conversation_controller.dart';
|
||||
import 'package:clide/builtin/claude/src/conversation_view.dart';
|
||||
import 'package:clide/builtin/claude/src/transcript_publisher.dart';
|
||||
@@ -137,6 +138,21 @@ void main() {
|
||||
expect(find.text('Waiting for Claude…'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('empty controller shows the provided emptyState instead', (tester) async {
|
||||
final stream = StreamController<ConversationItem>.broadcast();
|
||||
final c = ConversationController(stream: stream.stream);
|
||||
addTearDown(c.dispose);
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ConversationView(
|
||||
controller: c,
|
||||
emptyState: const ClideText('CUSTOM EMPTY'),
|
||||
),
|
||||
));
|
||||
expect(find.text('CUSTOM EMPTY'), findsOneWidget);
|
||||
expect(find.text('Waiting for Claude…'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('renders a card per item kind with role/tool labels', (tester) async {
|
||||
await pumpWith(tester, [
|
||||
_user('a question'),
|
||||
@@ -183,4 +199,27 @@ void main() {
|
||||
expect(copied, contains('answer text'));
|
||||
});
|
||||
});
|
||||
|
||||
group('ClaudeBanner', () {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
testWidgets('shows role, workspace, status, and a hint', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
const ClaudeBanner(
|
||||
role: 'primary',
|
||||
workspace: '/work/space',
|
||||
statusLine: 'tmux · clide-claude-x',
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
expect(find.text('Claude'), findsOneWidget);
|
||||
expect(find.text('primary'), findsOneWidget);
|
||||
expect(find.text('/work/space'), findsOneWidget);
|
||||
expect(find.text('tmux · clide-claude-x'), findsOneWidget);
|
||||
expect(find.textContaining('Warming up'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user