groove-dev 0.27.196 → 0.27.198

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 (106) hide show
  1. package/axom-integration/Screenshot_2026-07-25_at_5.25.30_PM.png +0 -0
  2. package/axom-integration/Screenshot_2026-07-25_at_5.33.26_PM.png +0 -0
  3. package/axom-integration/Screenshot_2026-07-25_at_6.11.53_PM.png +0 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/api.js +5 -5
  7. package/node_modules/@groove-dev/daemon/src/axom-connector.js +340 -0
  8. package/node_modules/@groove-dev/daemon/src/axom-install.js +140 -0
  9. package/node_modules/@groove-dev/daemon/src/axom-server.js +229 -0
  10. package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +6 -0
  12. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  13. package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +8 -1
  14. package/node_modules/@groove-dev/daemon/src/innerchat-relay.js +89 -0
  15. package/node_modules/@groove-dev/daemon/src/innerchat.js +261 -1
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  17. package/node_modules/@groove-dev/daemon/src/network-guard.js +56 -0
  18. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  19. package/node_modules/@groove-dev/daemon/src/providers/axom.js +66 -0
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
  21. package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
  22. package/node_modules/@groove-dev/daemon/src/routes/axom.js +196 -0
  23. package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
  24. package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +152 -20
  25. package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +412 -0
  26. package/node_modules/@groove-dev/daemon/test/axom-server.test.js +187 -0
  27. package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
  28. package/node_modules/@groove-dev/daemon/test/innerchat-relay.test.js +251 -0
  29. package/node_modules/@groove-dev/gui/dist/assets/index-RbtaI6l7.css +1 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  31. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  32. package/node_modules/@groove-dev/gui/package.json +1 -1
  33. package/node_modules/@groove-dev/gui/src/App.jsx +2 -2
  34. package/node_modules/@groove-dev/gui/src/app.css +53 -0
  35. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +2 -2
  36. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +3 -3
  37. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  38. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  39. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +1 -1
  40. package/node_modules/@groove-dev/gui/src/stores/groove.js +24 -163
  41. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
  42. package/node_modules/@groove-dev/gui/src/stores/slices/axom-slice.js +197 -0
  43. package/node_modules/@groove-dev/gui/src/views/axom.jsx +1060 -0
  44. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/views/settings.jsx +178 -64
  46. package/package.json +2 -2
  47. package/packages/cli/package.json +1 -1
  48. package/packages/daemon/package.json +1 -1
  49. package/packages/daemon/src/api.js +5 -5
  50. package/packages/daemon/src/axom-connector.js +340 -0
  51. package/packages/daemon/src/axom-install.js +140 -0
  52. package/packages/daemon/src/axom-server.js +229 -0
  53. package/packages/daemon/src/chatstore.js +104 -0
  54. package/packages/daemon/src/federation.js +6 -0
  55. package/packages/daemon/src/index.js +14 -2
  56. package/packages/daemon/src/innerchat-docs.js +8 -1
  57. package/packages/daemon/src/innerchat-relay.js +89 -0
  58. package/packages/daemon/src/innerchat.js +261 -1
  59. package/packages/daemon/src/introducer.js +1 -1
  60. package/packages/daemon/src/network-guard.js +56 -0
  61. package/packages/daemon/src/process.js +2 -2
  62. package/packages/daemon/src/providers/axom.js +66 -0
  63. package/packages/daemon/src/providers/index.js +2 -0
  64. package/packages/daemon/src/routes/agents.js +1 -175
  65. package/packages/daemon/src/routes/axom.js +196 -0
  66. package/packages/daemon/src/routes/chat-history.js +34 -0
  67. package/packages/daemon/src/routes/innerchat.js +152 -20
  68. package/packages/gui/dist/assets/index-RbtaI6l7.css +1 -0
  69. package/packages/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  70. package/packages/gui/dist/index.html +2 -2
  71. package/packages/gui/package.json +1 -1
  72. package/packages/gui/src/App.jsx +2 -2
  73. package/packages/gui/src/app.css +53 -0
  74. package/packages/gui/src/components/agents/agent-config.jsx +2 -2
  75. package/packages/gui/src/components/agents/spawn-wizard.jsx +3 -3
  76. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  77. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  78. package/packages/gui/src/components/layout/command-palette.jsx +1 -1
  79. package/packages/gui/src/stores/groove.js +24 -163
  80. package/packages/gui/src/stores/slices/agents-slice.js +52 -2
  81. package/packages/gui/src/stores/slices/axom-slice.js +197 -0
  82. package/packages/gui/src/views/axom.jsx +1060 -0
  83. package/packages/gui/src/views/marketplace.jsx +1 -1
  84. package/packages/gui/src/views/settings.jsx +178 -64
  85. package/node_modules/@groove-dev/daemon/src/conversations.js +0 -576
  86. package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +0 -1
  87. package/node_modules/@groove-dev/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  88. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +0 -198
  89. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +0 -367
  90. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +0 -527
  91. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +0 -212
  92. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +0 -163
  93. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +0 -201
  94. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +0 -227
  95. package/node_modules/@groove-dev/gui/src/views/chat.jsx +0 -6
  96. package/packages/daemon/src/conversations.js +0 -576
  97. package/packages/gui/dist/assets/index-Cat5pJUx.css +0 -1
  98. package/packages/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  99. package/packages/gui/src/components/chat/chat-header.jsx +0 -198
  100. package/packages/gui/src/components/chat/chat-input.jsx +0 -367
  101. package/packages/gui/src/components/chat/chat-messages.jsx +0 -527
  102. package/packages/gui/src/components/chat/chat-view.jsx +0 -212
  103. package/packages/gui/src/components/chat/conversation-list.jsx +0 -163
  104. package/packages/gui/src/components/chat/model-picker.jsx +0 -201
  105. package/packages/gui/src/stores/slices/chat-slice.js +0 -227
  106. package/packages/gui/src/views/chat.jsx +0 -6
@@ -1,212 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useCallback } from 'react';
3
- import { Plus, PanelLeftOpen } 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
- import { isImageModel } from './model-picker';
11
-
12
- function EmptyState({ onNewChat }) {
13
- return (
14
- <div className="flex-1 flex items-center justify-center">
15
- <div className="text-center space-y-4">
16
- <h1 className="text-lg font-semibold text-text-1 font-sans">Groove Chat</h1>
17
- <p className="text-sm text-text-3 font-sans">Every provider, every model, full project context.</p>
18
- <button
19
- onClick={onNewChat}
20
- className="inline-flex items-center gap-2 h-9 px-5 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"
21
- >
22
- <Plus size={14} />
23
- New Chat
24
- </button>
25
- </div>
26
- </div>
27
- );
28
- }
29
-
30
- export function ChatView() {
31
- const conversations = useGrooveStore((s) => s.conversations);
32
- const activeConversationId = useGrooveStore((s) => s.activeConversationId);
33
- const conversationMessages = useGrooveStore((s) => s.conversationMessages);
34
- const sendingMessage = useGrooveStore((s) => s.sendingMessage);
35
- const streamingConversationId = useGrooveStore((s) => s.streamingConversationId);
36
- const conversationActiveTools = useGrooveStore((s) => s.conversationActiveTools);
37
- const createConversation = useGrooveStore((s) => s.createConversation);
38
- const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
39
- const sendChatMessage = useGrooveStore((s) => s.sendChatMessage);
40
- const sendImageMessage = useGrooveStore((s) => s.sendImageMessage);
41
-
42
- const stopChatStreaming = useGrooveStore((s) => s.stopChatStreaming);
43
- const setConversationMode = useGrooveStore((s) => s.setConversationMode);
44
- const setConversationModel = useGrooveStore((s) => s.setConversationModel);
45
-
46
- const conversationRoles = useGrooveStore((s) => s.conversationRoles);
47
- const setConversationRole = useGrooveStore((s) => s.setConversationRole);
48
-
49
- const conversationReasoningEffort = useGrooveStore((s) => s.conversationReasoningEffort);
50
- const setConversationReasoningEffort = useGrooveStore((s) => s.setConversationReasoningEffort);
51
- const conversationVerbosity = useGrooveStore((s) => s.conversationVerbosity);
52
- const setConversationVerbosity = useGrooveStore((s) => s.setConversationVerbosity);
53
-
54
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
55
- const [replyContext, setReplyContext] = useState(null);
56
- const [modeChanging, setModeChanging] = useState(false);
57
-
58
- const activeRole = activeConversationId ? (conversationRoles?.[activeConversationId] || 'chat') : 'chat';
59
- const activeReasoningEffort = activeConversationId ? (conversationReasoningEffort?.[activeConversationId] || 'medium') : 'medium';
60
- const activeVerbosity = activeConversationId ? (conversationVerbosity?.[activeConversationId] || 'medium') : 'medium';
61
-
62
- const activeConversation = conversations.find((c) => c.id === activeConversationId) || null;
63
- const isCodexProvider = activeConversation?.provider === 'codex';
64
- const messages = activeConversationId ? (conversationMessages[activeConversationId] || []) : [];
65
- const isStreaming = streamingConversationId === activeConversationId && sendingMessage;
66
- const activeTool = activeConversationId ? (conversationActiveTools?.[activeConversationId] || null) : null;
67
- const currentModelIsImage = activeConversation ? isImageModel(activeConversation.model) : false;
68
-
69
- const handleNewChat = useCallback(async (provider, model) => {
70
- try {
71
- await createConversation(provider || null, model || null, 'api');
72
- } catch { /* toast handles */ }
73
- }, [createConversation]);
74
-
75
- const handleModeChange = useCallback(async (mode) => {
76
- if (!activeConversationId || modeChanging) return;
77
- setModeChanging(true);
78
- try {
79
- await setConversationMode(activeConversationId, mode);
80
- } finally {
81
- setModeChanging(false);
82
- }
83
- }, [activeConversationId, setConversationMode, modeChanging]);
84
-
85
- const handleRoleChange = useCallback((role) => {
86
- if (!activeConversationId) return;
87
- setConversationRole(activeConversationId, role);
88
- }, [activeConversationId, setConversationRole]);
89
-
90
- const handleSend = useCallback((text) => {
91
- if (!activeConversationId) return;
92
-
93
- if (currentModelIsImage) {
94
- const prompt = replyContext
95
- ? `${text} (iterating on: "${replyContext.prompt}")`
96
- : text;
97
- sendImageMessage(activeConversationId, prompt, { model: activeConversation.model });
98
- setReplyContext(null);
99
- } else {
100
- sendChatMessage(activeConversationId, text);
101
- }
102
- }, [activeConversationId, activeConversation, currentModelIsImage, replyContext, sendChatMessage, sendImageMessage]);
103
-
104
- const handleStop = useCallback(() => {
105
- if (!activeConversation) return;
106
- stopChatStreaming(activeConversationId);
107
- }, [activeConversation, activeConversationId, stopChatStreaming]);
108
-
109
- const handleModelChange = useCallback(async (selection) => {
110
- if (activeConversationId) {
111
- await setConversationModel(activeConversationId, selection.provider, selection.model);
112
- } else {
113
- await handleNewChat(selection.provider, selection.model);
114
- }
115
- }, [activeConversationId, setConversationModel, handleNewChat]);
116
-
117
- const handleImageReply = useCallback((msg) => {
118
- setReplyContext(msg);
119
- }, []);
120
-
121
- const handleReasoningEffortChange = useCallback((effort) => {
122
- if (!activeConversationId) return;
123
- setConversationReasoningEffort(activeConversationId, effort);
124
- }, [activeConversationId, setConversationReasoningEffort]);
125
-
126
- const handleVerbosityChange = useCallback((verbosity) => {
127
- if (!activeConversationId) return;
128
- setConversationVerbosity(activeConversationId, verbosity);
129
- }, [activeConversationId, setConversationVerbosity]);
130
-
131
- return (
132
- <div className="flex h-full bg-surface-0">
133
- {/* Conversation sidebar */}
134
- <div className={cn(
135
- 'relative flex-shrink-0 border-r border-accent/12 bg-surface-1 transition-all duration-200 overflow-hidden',
136
- sidebarCollapsed ? 'w-0 border-r-0' : 'w-64',
137
- )}>
138
- <ConversationList onNewChat={() => handleNewChat()} onCollapse={() => setSidebarCollapsed(true)} />
139
- </div>
140
-
141
- {/* Main chat area */}
142
- <div className="flex-1 flex flex-col min-w-0">
143
- {sidebarCollapsed && (
144
- <button
145
- onClick={() => setSidebarCollapsed(false)}
146
- className="absolute top-3 left-2 z-10 w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
147
- title="Show sidebar"
148
- >
149
- <PanelLeftOpen size={15} />
150
- </button>
151
- )}
152
-
153
- {activeConversation ? (
154
- <>
155
- <ChatHeader conversation={activeConversation} role={activeRole} onRoleChange={handleRoleChange} sidebarCollapsed={sidebarCollapsed} />
156
- <ChatMessages
157
- messages={messages}
158
- isStreaming={isStreaming}
159
- model={activeConversation.model}
160
- mode={activeConversation.mode || 'api'}
161
- onImageReply={handleImageReply}
162
- role={activeRole}
163
- activeTool={activeTool}
164
- />
165
- <ChatInput
166
- onSend={handleSend}
167
- onStop={handleStop}
168
- sending={sendingMessage}
169
- streaming={isStreaming}
170
- disabled={false}
171
- isImageModel={currentModelIsImage}
172
- currentModel={activeConversation.model}
173
- currentProvider={activeConversation.provider}
174
- onModelChange={handleModelChange}
175
- replyContext={replyContext}
176
- onClearReply={() => setReplyContext(null)}
177
- role={activeRole}
178
- isCodex={isCodexProvider}
179
- reasoningEffort={activeReasoningEffort}
180
- onReasoningEffortChange={handleReasoningEffortChange}
181
- verbosity={activeVerbosity}
182
- onVerbosityChange={handleVerbosityChange}
183
- mode={activeConversation?.mode || 'api'}
184
- onModeChange={handleModeChange}
185
- modeChanging={modeChanging}
186
- />
187
- </>
188
- ) : (
189
- <>
190
- <EmptyState onNewChat={() => handleNewChat()} />
191
- <ChatInput
192
- onSend={(text) => {
193
- handleNewChat().then(() => {
194
- setTimeout(() => {
195
- const id = useGrooveStore.getState().activeConversationId;
196
- if (id) sendChatMessage(id, text);
197
- }, 500);
198
- });
199
- }}
200
- onStop={() => {}}
201
- sending={false}
202
- streaming={false}
203
- disabled={false}
204
- isImageModel={false}
205
- currentModel={null}
206
- />
207
- </>
208
- )}
209
- </div>
210
- </div>
211
- );
212
- }
@@ -1,163 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useMemo } from 'react';
3
- import { SquarePen, MessageCircle, Pin, Pencil, PinOff, Trash2, Zap, Bot, PanelLeftClose } 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.5 text-left transition-colors cursor-pointer group border-b border-border-subtle/50',
47
- isActive
48
- ? 'text-text-0'
49
- : 'text-text-2 hover:bg-surface-3/40 hover:text-text-1',
50
- )}
51
- >
52
- <div className="flex-1 min-w-0">
53
- <div className="text-xs font-medium font-sans truncate">{conv.title || 'New Chat'}</div>
54
- <div className="flex items-center gap-1.5 mt-0.5">
55
- {conv.mode === 'agent'
56
- ? <Bot size={9} className="text-purple flex-shrink-0" />
57
- : <Zap size={9} className="text-accent flex-shrink-0" />
58
- }
59
- {conv.model && <Badge variant="default" className="text-[8px] px-1 py-0">{formatModelName(conv.model)}</Badge>}
60
- <span className="text-2xs text-text-4 font-sans">{timeAgo(conv.updatedAt || conv.createdAt)}</span>
61
- </div>
62
- </div>
63
- {conv.pinned && <Pin size={10} className="text-accent flex-shrink-0" />}
64
- </button>
65
- </ContextMenuTrigger>
66
- <ContextMenuContent>
67
- <ContextMenuItem onSelect={() => onRename(conv)}>
68
- <Pencil size={12} /> Rename
69
- </ContextMenuItem>
70
- <ContextMenuItem onSelect={() => onPin(conv)}>
71
- {conv.pinned ? <PinOff size={12} /> : <Pin size={12} />}
72
- {conv.pinned ? 'Unpin' : 'Pin'}
73
- </ContextMenuItem>
74
- <ContextMenuSeparator />
75
- <ContextMenuItem danger onSelect={() => onDelete(conv.id)}>
76
- <Trash2 size={12} /> Delete
77
- </ContextMenuItem>
78
- </ContextMenuContent>
79
- </ContextMenu>
80
- );
81
- }
82
-
83
- export function ConversationList({ onNewChat, onCollapse }) {
84
- const conversations = useGrooveStore((s) => s.conversations);
85
- const activeConversationId = useGrooveStore((s) => s.activeConversationId);
86
- const setActiveConversation = useGrooveStore((s) => s.setActiveConversation);
87
- const renameConversation = useGrooveStore((s) => s.renameConversation);
88
- const pinConversation = useGrooveStore((s) => s.pinConversation);
89
- const deleteConversation = useGrooveStore((s) => s.deleteConversation);
90
-
91
- const groups = useMemo(() => groupByDate(conversations), [conversations]);
92
-
93
- function handleRename(conv) {
94
- const name = prompt('Rename conversation:', conv.title || '');
95
- if (name && name.trim()) renameConversation(conv.id, name.trim());
96
- }
97
-
98
- function handlePin(conv) {
99
- pinConversation(conv.id, !conv.pinned);
100
- }
101
-
102
- const renderGroup = (label, items) => {
103
- if (items.length === 0) return null;
104
- return (
105
- <div key={label}>
106
- <GroupLabel label={label} />
107
- {items.map((conv) => (
108
- <ConversationItem
109
- key={conv.id}
110
- conv={conv}
111
- isActive={conv.id === activeConversationId}
112
- onSelect={setActiveConversation}
113
- onRename={handleRename}
114
- onPin={handlePin}
115
- onDelete={deleteConversation}
116
- />
117
- ))}
118
- </div>
119
- );
120
- };
121
-
122
- return (
123
- <div className="flex flex-col h-full">
124
- <div className="flex items-center justify-between px-3 pt-3 pb-2">
125
- <span className="text-xs font-semibold text-text-2 font-sans">Chats</span>
126
- <div className="flex items-center gap-0.5">
127
- <button
128
- onClick={onNewChat}
129
- className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
130
- title="New chat"
131
- >
132
- <SquarePen size={15} />
133
- </button>
134
- <button
135
- onClick={onCollapse}
136
- className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
137
- title="Collapse sidebar"
138
- >
139
- <PanelLeftClose size={15} />
140
- </button>
141
- </div>
142
- </div>
143
-
144
- <div className="flex-1 overflow-y-auto pt-1 pb-3">
145
- {conversations.length === 0 ? (
146
- <div className="flex flex-col items-center justify-center py-16 text-center px-4">
147
- <MessageCircle size={24} className="text-text-4 mb-3" />
148
- <p className="text-xs text-text-3 font-sans">No conversations yet</p>
149
- <p className="text-2xs text-text-4 font-sans mt-1">Start a new chat to begin</p>
150
- </div>
151
- ) : (
152
- <>
153
- {renderGroup('Pinned', groups.pinned)}
154
- {renderGroup('Today', groups.today)}
155
- {renderGroup('Yesterday', groups.yesterday)}
156
- {renderGroup('Previous 7 Days', groups.week)}
157
- {renderGroup('Older', groups.older)}
158
- </>
159
- )}
160
- </div>
161
- </div>
162
- );
163
- }
@@ -1,201 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useEffect, useRef } from 'react';
3
- import { ChevronDown, Globe, Cpu, Zap, Sparkles, Image as ImageIcon } 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
- export 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
- export function getTier(model) {
25
- const name = (model || '').toLowerCase();
26
- if (name.includes('gpt-5.5') || name.includes('gpt-5.4-pro')) return 'frontier';
27
- if (name.includes('gpt-5.4-mini') || name.includes('gpt-5-mini')) return 'mid';
28
- if (name.includes('gpt-5.4-nano') || name.includes('gpt-5-nano')) return 'fast';
29
- if (name.includes('gpt-5.4')) return 'frontier';
30
- if (name.includes('opus') || name.includes('pro') || name.includes('o3') || name.includes('gpt-4o')) return 'frontier';
31
- if (name.includes('sonnet') || name.includes('flash') || name.includes('o4-mini')) return 'mid';
32
- return 'fast';
33
- }
34
-
35
- export function getContextSize(model) {
36
- const name = (model || '').toLowerCase();
37
- if (name.startsWith('gpt-5')) return '200k';
38
- if (name.includes('opus') || name.includes('sonnet')) return '200k';
39
- if (name.includes('haiku')) return '200k';
40
- if (name.includes('pro')) return '1M';
41
- if (name.includes('flash')) return '1M';
42
- if (name.includes('o3') || name.includes('o4')) return '128k';
43
- return '128k';
44
- }
45
-
46
- export function isImageModel(modelId) {
47
- if (!modelId) return false;
48
- const id = modelId.toLowerCase();
49
- return id.includes('dall-e') || id.includes('imagen') || id.includes('gpt-image') || id.includes('imagine') || id.includes('nano-banana');
50
- }
51
-
52
- export function ModelPicker({ value, onChange, disabled }) {
53
- const [open, setOpen] = useState(false);
54
- const [providers, setProviders] = useState([]);
55
- const fetchProviders = useGrooveStore((s) => s.fetchProviders);
56
- const ref = useRef(null);
57
-
58
- useEffect(() => {
59
- fetchProviders().then((data) => {
60
- if (Array.isArray(data)) setProviders(data);
61
- else if (data?.providers) setProviders(data.providers);
62
- }).catch(() => {});
63
- }, [fetchProviders]);
64
-
65
- useEffect(() => {
66
- if (!open) return;
67
- function handleClick(e) {
68
- if (ref.current && !ref.current.contains(e.target)) setOpen(false);
69
- }
70
- document.addEventListener('mousedown', handleClick);
71
- return () => document.removeEventListener('mousedown', handleClick);
72
- }, [open]);
73
-
74
- const currentModel = value?.model || '';
75
- const currentModelDisplay = currentModel ? formatModelName(currentModel) : 'Select model';
76
- const currentProvider = value?.provider || '';
77
- const isNetwork = currentProvider === 'groove-network';
78
- const isCurrentImage = isImageModel(currentModel);
79
-
80
- const chatModels = [];
81
- const imageModels = [];
82
-
83
- for (const provider of providers) {
84
- const models = provider.models || [];
85
- const chatGroup = [];
86
- const imgGroup = [];
87
- for (const model of models) {
88
- const modelId = typeof model === 'string' ? model : model.id || model.name;
89
- const modelType = typeof model === 'object' ? model.type : undefined;
90
- if (modelType === 'image' || isImageModel(modelId)) {
91
- imgGroup.push(model);
92
- } else {
93
- chatGroup.push(model);
94
- }
95
- }
96
- if (chatGroup.length > 0) chatModels.push({ ...provider, models: chatGroup });
97
- if (imgGroup.length > 0) imageModels.push({ ...provider, models: imgGroup });
98
- }
99
-
100
- function renderModelGroup(providerList, sectionLabel) {
101
- if (providerList.length === 0) return null;
102
- return (
103
- <>
104
- {sectionLabel && (
105
- <div className="px-3 py-1.5 text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans bg-surface-0 border-b border-border-subtle flex items-center gap-1.5">
106
- {sectionLabel === 'Image Generation' && <ImageIcon size={10} className="text-purple" />}
107
- {sectionLabel}
108
- </div>
109
- )}
110
- {providerList.map((provider) => {
111
- const isNetworkProvider = provider.id === 'groove-network';
112
- return (
113
- <div key={provider.id}>
114
- <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">
115
- {isNetworkProvider && <Globe size={10} className="text-purple" />}
116
- {provider.name || provider.id}
117
- </div>
118
- {provider.models.map((model) => {
119
- const modelId = typeof model === 'string' ? model : model.id || model.name;
120
- const modelDisplayName = typeof model === 'string' ? model : model.name || model.id;
121
- const modelType = typeof model === 'object' ? model.type : undefined;
122
- const isImg = modelType === 'image' || isImageModel(modelId);
123
- const tier = isImg ? null : getTier(modelId);
124
- const tierConfig = tier ? TIER_CONFIG[tier] : null;
125
- const TierIcon = tierConfig?.icon;
126
- const isActive = currentModel === modelId && currentProvider === provider.id;
127
- return (
128
- <button
129
- key={modelId}
130
- onClick={() => {
131
- onChange({ provider: provider.id, model: modelId });
132
- setOpen(false);
133
- }}
134
- className={cn(
135
- 'w-full flex items-center gap-2 px-3 py-2 text-left transition-colors cursor-pointer',
136
- isActive ? 'bg-accent/10 text-text-0' : 'hover:bg-surface-3 text-text-1',
137
- )}
138
- >
139
- <div className="flex-1 min-w-0">
140
- <div className="flex items-center gap-1.5">
141
- {isImg && <ImageIcon size={11} className="text-purple flex-shrink-0" />}
142
- <span className="text-xs font-medium font-sans truncate">{modelDisplayName}</span>
143
- </div>
144
- {!isImg && <div className="text-2xs text-text-4 font-sans">{getContextSize(modelId)} context</div>}
145
- </div>
146
- <div className="flex items-center gap-1.5 flex-shrink-0">
147
- {isNetworkProvider && (
148
- <Badge variant="purple" className="text-[9px]">
149
- <Globe size={8} /> Decentralized
150
- </Badge>
151
- )}
152
- {isImg ? (
153
- <Badge variant="purple" className="text-[9px]">
154
- <ImageIcon size={8} /> Image
155
- </Badge>
156
- ) : tierConfig && (
157
- <Badge variant={tierConfig.variant} className="text-[9px]">
158
- <TierIcon size={8} /> {tierConfig.label}
159
- </Badge>
160
- )}
161
- </div>
162
- </button>
163
- );
164
- })}
165
- </div>
166
- );
167
- })}
168
- </>
169
- );
170
- }
171
-
172
- return (
173
- <div ref={ref} className="relative">
174
- <button
175
- onClick={() => !disabled && setOpen(!open)}
176
- disabled={disabled}
177
- className={cn(
178
- 'flex items-center gap-1.5 h-8 px-2.5 rounded-lg text-xs font-medium font-sans transition-colors cursor-pointer',
179
- 'bg-surface-4 border border-border-subtle hover:bg-surface-5',
180
- 'disabled:opacity-40 disabled:cursor-not-allowed',
181
- isNetwork && 'border-purple/30 bg-purple/8',
182
- isCurrentImage && 'border-purple/30 bg-purple/8',
183
- )}
184
- >
185
- {isCurrentImage ? <ImageIcon size={12} className="text-purple" /> : isNetwork ? <Globe size={12} className="text-purple" /> : <Cpu size={12} className="text-text-3" />}
186
- <span className={cn('max-w-[120px] truncate', isCurrentImage ? 'text-purple' : 'text-text-1')}>{currentModelDisplay}</span>
187
- <ChevronDown size={12} className="text-text-4" />
188
- </button>
189
-
190
- {open && (
191
- <div className="absolute top-full right-0 mt-1 w-72 max-h-80 overflow-y-auto rounded-lg border border-border bg-surface-1 shadow-xl z-50">
192
- {providers.length === 0 && (
193
- <div className="px-4 py-6 text-center text-xs text-text-3 font-sans">No providers available</div>
194
- )}
195
- {renderModelGroup(chatModels, null)}
196
- {renderModelGroup(imageModels, 'Image Generation')}
197
- </div>
198
- )}
199
- </div>
200
- );
201
- }