groove-dev 0.27.55 → 0.27.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/ai-chat/CHAT_MASTER_PLAN.md +184 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +169 -0
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +423 -0
  6. package/node_modules/@groove-dev/daemon/src/index.js +2 -0
  7. package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +1 -0
  8. package/node_modules/@groove-dev/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
  9. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  10. package/node_modules/@groove-dev/gui/package.json +1 -1
  11. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  12. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +138 -0
  13. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +112 -0
  14. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +347 -0
  15. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +165 -0
  16. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +154 -0
  17. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +143 -0
  18. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +220 -0
  20. package/node_modules/@groove-dev/gui/src/views/chat.jsx +6 -0
  21. package/package.json +1 -1
  22. package/packages/cli/package.json +1 -1
  23. package/packages/daemon/package.json +1 -1
  24. package/packages/daemon/src/api.js +169 -0
  25. package/packages/daemon/src/conversations.js +423 -0
  26. package/packages/daemon/src/index.js +2 -0
  27. package/packages/gui/dist/assets/index-C5WTeZO4.css +1 -0
  28. package/packages/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
  29. package/packages/gui/dist/index.html +2 -2
  30. package/packages/gui/package.json +1 -1
  31. package/packages/gui/src/app.jsx +2 -0
  32. package/packages/gui/src/components/chat/chat-header.jsx +138 -0
  33. package/packages/gui/src/components/chat/chat-input.jsx +112 -0
  34. package/packages/gui/src/components/chat/chat-messages.jsx +347 -0
  35. package/packages/gui/src/components/chat/chat-view.jsx +165 -0
  36. package/packages/gui/src/components/chat/conversation-list.jsx +154 -0
  37. package/packages/gui/src/components/chat/model-picker.jsx +143 -0
  38. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  39. package/packages/gui/src/stores/groove.js +220 -0
  40. package/packages/gui/src/views/chat.jsx +6 -0
  41. package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.css +0 -1
  42. package/packages/gui/dist/assets/index-CyVj0fHl.css +0 -1
@@ -0,0 +1,165 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useCallback } from 'react';
3
+ import { MessageCircle, Plus, Sparkles, Zap } from 'lucide-react';
4
+ import { useGrooveStore } from '../../stores/groove';
5
+ import { cn } from '../../lib/cn';
6
+ import { ConversationList } from './conversation-list';
7
+ import { ChatHeader } from './chat-header';
8
+ import { ChatMessages } from './chat-messages';
9
+ import { ChatInput } from './chat-input';
10
+
11
+ function EmptyState({ onNewChat }) {
12
+ return (
13
+ <div className="flex-1 flex items-center justify-center">
14
+ <div className="max-w-md w-full text-center space-y-8 px-8">
15
+ <div className="relative mx-auto w-20 h-20">
16
+ <div className="absolute inset-0 rounded-full bg-accent/8 animate-pulse" />
17
+ <div className="absolute inset-1 rounded-full bg-surface-3 border border-border-subtle flex items-center justify-center shadow-lg shadow-accent/5">
18
+ <MessageCircle size={32} className="text-accent" />
19
+ </div>
20
+ </div>
21
+
22
+ <div className="space-y-3">
23
+ <h1 className="text-2xl font-bold text-text-0 font-sans tracking-tight">Groove Chat</h1>
24
+ <p className="text-sm text-text-2 font-sans max-w-sm mx-auto leading-relaxed">
25
+ A command center disguised as a conversation. Every provider, every model, full project context.
26
+ </p>
27
+ </div>
28
+
29
+ <button
30
+ onClick={onNewChat}
31
+ className="inline-flex items-center gap-2 h-10 px-6 rounded-lg bg-accent/15 text-accent text-sm font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
32
+ >
33
+ <Plus size={16} />
34
+ New Chat
35
+ </button>
36
+
37
+ <div className="grid grid-cols-2 gap-3 max-w-sm mx-auto">
38
+ <div className="flex items-center gap-2 p-3 rounded-lg bg-surface-3 border border-border-subtle">
39
+ <Sparkles size={14} className="text-purple flex-shrink-0" />
40
+ <span className="text-2xs text-text-2 font-sans">Multi-model routing</span>
41
+ </div>
42
+ <div className="flex items-center gap-2 p-3 rounded-lg bg-surface-3 border border-border-subtle">
43
+ <Zap size={14} className="text-warning flex-shrink-0" />
44
+ <span className="text-2xs text-text-2 font-sans">Streaming responses</span>
45
+ </div>
46
+ </div>
47
+
48
+ <p className="text-xs text-text-4 font-sans">
49
+ <kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+Shift+N</kbd>
50
+ <span className="mx-1.5">new chat</span>
51
+ </p>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function ChatView() {
58
+ const conversations = useGrooveStore((s) => s.conversations);
59
+ const activeConversationId = useGrooveStore((s) => s.activeConversationId);
60
+ const conversationMessages = useGrooveStore((s) => s.conversationMessages);
61
+ const sendingMessage = useGrooveStore((s) => s.sendingMessage);
62
+ const streamingConversationId = useGrooveStore((s) => s.streamingConversationId);
63
+ const createConversation = useGrooveStore((s) => s.createConversation);
64
+ const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
65
+ const sendChatMessage = useGrooveStore((s) => s.sendChatMessage);
66
+ const stopAgent = useGrooveStore((s) => s.stopAgent);
67
+ const stopChatStreaming = useGrooveStore((s) => s.stopChatStreaming);
68
+ const setConversationMode = useGrooveStore((s) => s.setConversationMode);
69
+
70
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
71
+
72
+ const activeConversation = conversations.find((c) => c.id === activeConversationId) || null;
73
+ const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
74
+ const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
75
+
76
+ const handleNewChat = useCallback(async (provider, model) => {
77
+ const p = provider || 'claude-code';
78
+ const m = model || 'claude-sonnet-4-6';
79
+ try {
80
+ await createConversation(p, m, 'api');
81
+ } catch { /* toast handles */ }
82
+ }, [createConversation]);
83
+
84
+ const handleModeChange = useCallback((mode) => {
85
+ if (!activeConversationId) return;
86
+ setConversationMode(activeConversationId, mode);
87
+ }, [activeConversationId, setConversationMode]);
88
+
89
+ const handleSend = useCallback((text) => {
90
+ if (!activeConversationId) return;
91
+ sendChatMessage(activeConversationId, text);
92
+ }, [activeConversationId, sendChatMessage]);
93
+
94
+ const handleStop = useCallback(() => {
95
+ if (!activeConversation) return;
96
+ if (activeConversation.mode === 'agent' && activeConversation.agentId) {
97
+ stopAgent(activeConversation.agentId);
98
+ } else {
99
+ stopChatStreaming(activeConversationId);
100
+ }
101
+ }, [activeConversation, activeConversationId, stopAgent, stopChatStreaming]);
102
+
103
+ const handleModelChange = useCallback(async (selection) => {
104
+ if (activeConversationId) {
105
+ // TODO: Update conversation model via API
106
+ } else {
107
+ await handleNewChat(selection.provider, selection.model);
108
+ }
109
+ }, [activeConversationId, handleNewChat]);
110
+
111
+ const currentModel = activeConversation
112
+ ? { provider: activeConversation.provider, model: activeConversation.model }
113
+ : null;
114
+
115
+ return (
116
+ <div className="flex h-full bg-surface-2">
117
+ {/* Conversation sidebar */}
118
+ <div className={cn(
119
+ 'flex-shrink-0 border-r border-border bg-surface-1 transition-all duration-200 overflow-hidden',
120
+ sidebarCollapsed ? 'w-0' : 'w-64',
121
+ )}>
122
+ <ConversationList onNewChat={() => handleNewChat()} />
123
+ </div>
124
+
125
+ {/* Main chat area */}
126
+ <div className="flex-1 flex flex-col min-w-0">
127
+ {activeConversation ? (
128
+ <>
129
+ <ChatHeader conversation={activeConversation} model={currentModel} onModelChange={handleModelChange} onModeChange={handleModeChange} />
130
+ <ChatMessages
131
+ messages={messages}
132
+ isStreaming={isStreaming}
133
+ model={activeConversation.model}
134
+ />
135
+ <ChatInput
136
+ onSend={handleSend}
137
+ onStop={handleStop}
138
+ sending={sendingMessage}
139
+ streaming={isStreaming}
140
+ disabled={false}
141
+ />
142
+ </>
143
+ ) : (
144
+ <>
145
+ <EmptyState onNewChat={() => handleNewChat()} />
146
+ <ChatInput
147
+ onSend={(text) => {
148
+ handleNewChat().then(() => {
149
+ setTimeout(() => {
150
+ const id = useGrooveStore.getState().activeConversationId;
151
+ if (id) sendChatMessage(id, text);
152
+ }, 500);
153
+ });
154
+ }}
155
+ onStop={() => {}}
156
+ sending={false}
157
+ streaming={false}
158
+ disabled={false}
159
+ />
160
+ </>
161
+ )}
162
+ </div>
163
+ </div>
164
+ );
165
+ }
@@ -0,0 +1,154 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useMemo } from 'react';
3
+ import { Plus, MessageCircle, Pin, Pencil, PinOff, Trash2, Zap, Bot } from 'lucide-react';
4
+ import { useGrooveStore } from '../../stores/groove';
5
+ import { cn } from '../../lib/cn';
6
+ import { Badge } from '../ui/badge';
7
+ import { timeAgo } from '../../lib/format';
8
+ import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../ui/context-menu';
9
+ import { formatModelName } from './model-picker';
10
+
11
+ function groupByDate(conversations) {
12
+ const now = new Date();
13
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
14
+ const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
15
+ const weekAgo = new Date(today); weekAgo.setDate(today.getDate() - 7);
16
+
17
+ const groups = { pinned: [], today: [], yesterday: [], week: [], older: [] };
18
+
19
+ for (const conv of conversations) {
20
+ if (conv.pinned) { groups.pinned.push(conv); continue; }
21
+ const d = new Date(conv.updatedAt || conv.createdAt);
22
+ if (d >= today) groups.today.push(conv);
23
+ else if (d >= yesterday) groups.yesterday.push(conv);
24
+ else if (d >= weekAgo) groups.week.push(conv);
25
+ else groups.older.push(conv);
26
+ }
27
+
28
+ return groups;
29
+ }
30
+
31
+ function GroupLabel({ label }) {
32
+ return (
33
+ <div className="px-3 pt-4 pb-1.5">
34
+ <span className="text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans">{label}</span>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ function ConversationItem({ conv, isActive, onSelect, onRename, onPin, onDelete }) {
40
+ return (
41
+ <ContextMenu>
42
+ <ContextMenuTrigger asChild>
43
+ <button
44
+ onClick={() => onSelect(conv.id)}
45
+ className={cn(
46
+ 'w-full flex items-center gap-2 px-3 py-2 text-left rounded-md transition-colors cursor-pointer group',
47
+ isActive
48
+ ? 'bg-accent/10 text-text-0'
49
+ : 'text-text-2 hover:bg-surface-4 hover:text-text-1',
50
+ )}
51
+ >
52
+ <MessageCircle size={13} className={cn('flex-shrink-0', isActive ? 'text-accent' : 'text-text-4 group-hover:text-text-3')} />
53
+ <div className="flex-1 min-w-0">
54
+ <div className="text-xs font-medium font-sans truncate">{conv.title || 'New Chat'}</div>
55
+ <div className="flex items-center gap-1.5 mt-0.5">
56
+ {conv.mode === 'agent'
57
+ ? <Bot size={9} className="text-purple flex-shrink-0" />
58
+ : <Zap size={9} className="text-accent flex-shrink-0" />
59
+ }
60
+ {conv.model && <Badge variant="default" className="text-[8px] px-1 py-0">{formatModelName(conv.model)}</Badge>}
61
+ <span className="text-2xs text-text-4 font-sans">{timeAgo(conv.updatedAt || conv.createdAt)}</span>
62
+ </div>
63
+ </div>
64
+ {conv.pinned && <Pin size={10} className="text-accent flex-shrink-0" />}
65
+ </button>
66
+ </ContextMenuTrigger>
67
+ <ContextMenuContent>
68
+ <ContextMenuItem onSelect={() => onRename(conv)}>
69
+ <Pencil size={12} /> Rename
70
+ </ContextMenuItem>
71
+ <ContextMenuItem onSelect={() => onPin(conv)}>
72
+ {conv.pinned ? <PinOff size={12} /> : <Pin size={12} />}
73
+ {conv.pinned ? 'Unpin' : 'Pin'}
74
+ </ContextMenuItem>
75
+ <ContextMenuSeparator />
76
+ <ContextMenuItem danger onSelect={() => onDelete(conv.id)}>
77
+ <Trash2 size={12} /> Delete
78
+ </ContextMenuItem>
79
+ </ContextMenuContent>
80
+ </ContextMenu>
81
+ );
82
+ }
83
+
84
+ export function ConversationList({ onNewChat }) {
85
+ const conversations = useGrooveStore((s) => s.conversations);
86
+ const activeConversationId = useGrooveStore((s) => s.activeConversationId);
87
+ const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
88
+ const renameConversation = useGrooveStore((s) => s.renameConversation);
89
+ const pinConversation = useGrooveStore((s) => s.pinConversation);
90
+ const deleteConversation = useGrooveStore((s) => s.deleteConversation);
91
+
92
+ const groups = useMemo(() => groupByDate(conversations), [conversations]);
93
+
94
+ function handleRename(conv) {
95
+ const name = prompt('Rename conversation:', conv.title || '');
96
+ if (name && name.trim()) renameConversation(conv.id, name.trim());
97
+ }
98
+
99
+ function handlePin(conv) {
100
+ pinConversation(conv.id, !conv.pinned);
101
+ }
102
+
103
+ const renderGroup = (label, items) => {
104
+ if (items.length === 0) return null;
105
+ return (
106
+ <div key={label}>
107
+ <GroupLabel label={label} />
108
+ {items.map((conv) => (
109
+ <ConversationItem
110
+ key={conv.id}
111
+ conv={conv}
112
+ isActive={conv.id === activeConversationId}
113
+ onSelect={setActiveConversation}
114
+ onRename={handleRename}
115
+ onPin={handlePin}
116
+ onDelete={deleteConversation}
117
+ />
118
+ ))}
119
+ </div>
120
+ );
121
+ };
122
+
123
+ return (
124
+ <div className="flex flex-col h-full">
125
+ <div className="flex-1 overflow-y-auto px-1.5 pt-3 pb-3 space-y-0.5">
126
+ {conversations.length === 0 ? (
127
+ <div className="flex flex-col items-center justify-center py-16 text-center px-4">
128
+ <MessageCircle size={24} className="text-text-4 mb-3" />
129
+ <p className="text-xs text-text-3 font-sans">No conversations yet</p>
130
+ <p className="text-2xs text-text-4 font-sans mt-1">Start a new chat to begin</p>
131
+ </div>
132
+ ) : (
133
+ <>
134
+ {renderGroup('Pinned', groups.pinned)}
135
+ {renderGroup('Today', groups.today)}
136
+ {renderGroup('Yesterday', groups.yesterday)}
137
+ {renderGroup('Previous 7 Days', groups.week)}
138
+ {renderGroup('Older', groups.older)}
139
+ </>
140
+ )}
141
+ </div>
142
+
143
+ <div className="p-3 border-t border-border-subtle">
144
+ <button
145
+ onClick={onNewChat}
146
+ className="w-full flex items-center justify-center gap-2 h-9 rounded-lg bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer border border-accent/20"
147
+ >
148
+ <Plus size={14} />
149
+ New Chat
150
+ </button>
151
+ </div>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,143 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { ChevronDown, Globe, Cpu, Zap, Sparkles } from 'lucide-react';
4
+ import { useGrooveStore } from '../../stores/groove';
5
+ import { cn } from '../../lib/cn';
6
+ import { Badge } from '../ui/badge';
7
+
8
+ export function formatModelName(id) {
9
+ if (!id) return '';
10
+ return id
11
+ .replace(/^claude-/, '')
12
+ .replace(/-(\d)/, ' $1')
13
+ .split('-')
14
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
15
+ .join(' ');
16
+ }
17
+
18
+ const TIER_CONFIG = {
19
+ frontier: { label: 'Frontier', variant: 'purple', icon: Sparkles },
20
+ mid: { label: 'Mid', variant: 'accent', icon: Zap },
21
+ fast: { label: 'Fast', variant: 'success', icon: Zap },
22
+ };
23
+
24
+ function getTier(model) {
25
+ const name = (model || '').toLowerCase();
26
+ if (name.includes('opus') || name.includes('pro') || name.includes('o3') || name.includes('gpt-4o')) return 'frontier';
27
+ if (name.includes('sonnet') || name.includes('flash') || name.includes('o4-mini')) return 'mid';
28
+ return 'fast';
29
+ }
30
+
31
+ function getContextSize(model) {
32
+ const name = (model || '').toLowerCase();
33
+ if (name.includes('opus') || name.includes('sonnet')) return '200k';
34
+ if (name.includes('haiku')) return '200k';
35
+ if (name.includes('pro')) return '1M';
36
+ if (name.includes('flash')) return '1M';
37
+ if (name.includes('o3') || name.includes('o4')) return '128k';
38
+ return '128k';
39
+ }
40
+
41
+ export function ModelPicker({ value, onChange, disabled }) {
42
+ const [open, setOpen] = useState(false);
43
+ const [providers, setProviders] = useState([]);
44
+ const fetchProviders = useGrooveStore((s) => s.fetchProviders);
45
+ const ref = useRef(null);
46
+
47
+ useEffect(() => {
48
+ fetchProviders().then((data) => {
49
+ if (Array.isArray(data)) setProviders(data);
50
+ else if (data?.providers) setProviders(data.providers);
51
+ }).catch(() => {});
52
+ }, [fetchProviders]);
53
+
54
+ useEffect(() => {
55
+ if (!open) return;
56
+ function handleClick(e) {
57
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
58
+ }
59
+ document.addEventListener('mousedown', handleClick);
60
+ return () => document.removeEventListener('mousedown', handleClick);
61
+ }, [open]);
62
+
63
+ const currentModel = value?.model || '';
64
+ const currentModelDisplay = currentModel ? formatModelName(currentModel) : 'Select model';
65
+ const currentProvider = value?.provider || '';
66
+ const isNetwork = currentProvider === 'groove-network';
67
+
68
+ return (
69
+ <div ref={ref} className="relative">
70
+ <button
71
+ onClick={() => !disabled && setOpen(!open)}
72
+ disabled={disabled}
73
+ className={cn(
74
+ 'flex items-center gap-1.5 h-8 px-2.5 rounded-lg text-xs font-medium font-sans transition-colors cursor-pointer',
75
+ 'bg-surface-4 border border-border-subtle hover:bg-surface-5',
76
+ 'disabled:opacity-40 disabled:cursor-not-allowed',
77
+ isNetwork && 'border-purple/30 bg-purple/8',
78
+ )}
79
+ >
80
+ {isNetwork ? <Globe size={12} className="text-purple" /> : <Cpu size={12} className="text-text-3" />}
81
+ <span className="text-text-1 max-w-[120px] truncate">{currentModelDisplay}</span>
82
+ <ChevronDown size={12} className="text-text-4" />
83
+ </button>
84
+
85
+ {open && (
86
+ <div className="absolute top-full left-0 mt-1 w-72 max-h-80 overflow-y-auto rounded-lg border border-border bg-surface-1 shadow-xl z-50">
87
+ {providers.length === 0 && (
88
+ <div className="px-4 py-6 text-center text-xs text-text-3 font-sans">No providers available</div>
89
+ )}
90
+ {providers.map((provider) => {
91
+ const models = provider.models || [];
92
+ if (models.length === 0) return null;
93
+ const isNetworkProvider = provider.id === 'groove-network';
94
+ return (
95
+ <div key={provider.id}>
96
+ <div className="px-3 py-1.5 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2 border-b border-border-subtle flex items-center gap-1.5">
97
+ {isNetworkProvider && <Globe size={10} className="text-purple" />}
98
+ {provider.name || provider.id}
99
+ </div>
100
+ {models.map((model) => {
101
+ const modelId = typeof model === 'string' ? model : model.id || model.name;
102
+ const modelDisplayName = typeof model === 'string' ? model : model.name || model.id;
103
+ const tier = getTier(modelId);
104
+ const tierConfig = TIER_CONFIG[tier];
105
+ const TierIcon = tierConfig.icon;
106
+ const isActive = currentModel === modelId && currentProvider === provider.id;
107
+ return (
108
+ <button
109
+ key={modelId}
110
+ onClick={() => {
111
+ onChange({ provider: provider.id, model: modelId });
112
+ setOpen(false);
113
+ }}
114
+ className={cn(
115
+ 'w-full flex items-center gap-2 px-3 py-2 text-left transition-colors cursor-pointer',
116
+ isActive ? 'bg-accent/10 text-text-0' : 'hover:bg-surface-3 text-text-1',
117
+ )}
118
+ >
119
+ <div className="flex-1 min-w-0">
120
+ <div className="text-xs font-medium font-sans truncate">{modelDisplayName}</div>
121
+ <div className="text-2xs text-text-4 font-sans">{getContextSize(modelId)} context</div>
122
+ </div>
123
+ <div className="flex items-center gap-1.5 flex-shrink-0">
124
+ {isNetworkProvider && (
125
+ <Badge variant="purple" className="text-[9px]">
126
+ <Globe size={8} /> Decentralized
127
+ </Badge>
128
+ )}
129
+ <Badge variant={tierConfig.variant} className="text-[9px]">
130
+ <TierIcon size={8} /> {tierConfig.label}
131
+ </Badge>
132
+ </div>
133
+ </button>
134
+ );
135
+ })}
136
+ </div>
137
+ );
138
+ })}
139
+ </div>
140
+ )}
141
+ </div>
142
+ );
143
+ }
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, Newspaper, Settings, Globe } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, Gamepad2, Users, Box, Newspaper, Settings, Globe, MessageCircle } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { Tooltip } from '../ui/tooltip';
5
5
  import { useGrooveStore } from '../../stores/groove';
@@ -13,6 +13,7 @@ const BASE_NAV_ITEMS = [
13
13
  { id: 'toys', icon: Gamepad2, label: 'Toys' },
14
14
  { id: 'models', icon: Box, label: 'Models' },
15
15
  { id: 'teams', icon: Users, label: 'Teams' },
16
+ { id: 'chat', icon: MessageCircle, label: 'Chat' },
16
17
  ];
17
18
 
18
19
  const NETWORK_NAV_ITEM = { id: 'network', icon: Globe, label: 'Network' };