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,198 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useRef, useEffect } from 'react';
3
- import { Pencil, Pin, PinOff, Trash2, Hash, MoreHorizontal, ChevronDown, X } from 'lucide-react';
4
- import { useGrooveStore } from '../../stores/groove';
5
- import { fmtNum } from '../../lib/format';
6
- import { cn } from '../../lib/cn';
7
- import { roleColor } from '../../lib/status';
8
-
9
- const CHAT_ROLES = [
10
- { id: 'chat', label: 'Chat', desc: 'General conversation' },
11
- { id: 'research', label: 'Research Assistant', desc: 'Explore ideas, web search' },
12
- { id: 'fullstack', label: 'Fullstack', desc: 'End-to-end engineering' },
13
- { id: 'backend', label: 'Backend', desc: 'APIs, services, databases' },
14
- { id: 'frontend', label: 'Frontend', desc: 'UI, components, styling' },
15
- { id: 'devops', label: 'DevOps', desc: 'CI/CD, infra, deployment' },
16
- { id: 'security', label: 'Security', desc: 'Audits, vulnerabilities' },
17
- { id: 'database', label: 'Database', desc: 'Schema, migrations, queries' },
18
- { id: 'testing', label: 'Testing', desc: 'Tests, coverage, QA' },
19
- { id: 'docs', label: 'Docs', desc: 'Documentation, guides' },
20
- { id: 'cmo', label: 'CMO', desc: 'Marketing, content, growth' },
21
- { id: 'cfo', label: 'CFO', desc: 'Finance, metrics, forecasting' },
22
- { id: 'ea', label: 'EA', desc: 'Executive assistant' },
23
- { id: 'analyst', label: 'Analyst', desc: 'Data analysis, insights' },
24
- { id: 'creative', label: 'Writer', desc: 'Copy, articles, proposals' },
25
- { id: 'support', label: 'Support', desc: 'Customer support, FAQs' },
26
- ];
27
-
28
- export function ChatHeader({ conversation, role, onRoleChange, sidebarCollapsed }) {
29
- const renameConversation = useGrooveStore((s) => s.renameConversation);
30
- const pinConversation = useGrooveStore((s) => s.pinConversation);
31
- const deleteConversation = useGrooveStore((s) => s.deleteConversation);
32
-
33
- const [editing, setEditing] = useState(false);
34
- const [title, setTitle] = useState(conversation.title || '');
35
- const [menuOpen, setMenuOpen] = useState(false);
36
- const [roleMenuOpen, setRoleMenuOpen] = useState(false);
37
- const inputRef = useRef(null);
38
- const menuRef = useRef(null);
39
- const roleMenuRef = useRef(null);
40
-
41
- useEffect(() => {
42
- setTitle(conversation.title || '');
43
- setEditing(false);
44
- }, [conversation.id, conversation.title]);
45
-
46
- useEffect(() => {
47
- if (editing && inputRef.current) inputRef.current.focus();
48
- }, [editing]);
49
-
50
- useEffect(() => {
51
- if (!menuOpen) return;
52
- function handleClick(e) {
53
- if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
54
- }
55
- document.addEventListener('mousedown', handleClick);
56
- return () => document.removeEventListener('mousedown', handleClick);
57
- }, [menuOpen]);
58
-
59
- useEffect(() => {
60
- if (!roleMenuOpen) return;
61
- function handleClick(e) {
62
- if (roleMenuRef.current && !roleMenuRef.current.contains(e.target)) setRoleMenuOpen(false);
63
- }
64
- document.addEventListener('mousedown', handleClick);
65
- return () => document.removeEventListener('mousedown', handleClick);
66
- }, [roleMenuOpen]);
67
-
68
- function handleRename() {
69
- const trimmed = title.trim();
70
- if (trimmed && trimmed !== conversation.title) {
71
- renameConversation(conversation.id, trimmed);
72
- } else {
73
- setTitle(conversation.title || '');
74
- }
75
- setEditing(false);
76
- }
77
-
78
- const agent = useGrooveStore((s) => s.agents.find((a) => a.id === conversation.agentId));
79
- const tokens = agent?.tokensUsed || 0;
80
-
81
- return (
82
- <div className={cn("h-12 flex items-center gap-3 border-b border-border-subtle bg-surface-0/80 flex-shrink-0", sidebarCollapsed ? 'pl-10 pr-4' : 'px-4')}>
83
- <Hash size={14} className="text-text-4 flex-shrink-0" />
84
-
85
- {editing ? (
86
- <input
87
- ref={inputRef}
88
- value={title}
89
- onChange={(e) => setTitle(e.target.value)}
90
- onKeyDown={(e) => { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') { setTitle(conversation.title || ''); setEditing(false); } }}
91
- onBlur={handleRename}
92
- className="flex-1 min-w-0 bg-transparent text-sm font-semibold text-text-0 font-sans outline-none border-b border-accent"
93
- maxLength={100}
94
- />
95
- ) : (
96
- <button
97
- onClick={() => setEditing(true)}
98
- className="flex-1 min-w-0 text-left text-sm font-semibold text-text-0 font-sans truncate hover:text-accent transition-colors cursor-pointer"
99
- >
100
- {conversation.title || 'New Chat'}
101
- </button>
102
- )}
103
-
104
- <div className="w-px h-5 bg-border-subtle" />
105
-
106
- <div className="flex items-center gap-2 flex-shrink-0">
107
- <div ref={roleMenuRef} className="relative">
108
- <button
109
- onClick={() => setRoleMenuOpen(!roleMenuOpen)}
110
- className={cn(
111
- 'flex items-center gap-1.5 h-7 px-2.5 rounded-lg text-2xs font-semibold font-sans transition-colors cursor-pointer border',
112
- role
113
- ? 'border-current/20'
114
- : 'bg-surface-3 border-border-subtle text-text-3 hover:text-text-1',
115
- )}
116
- style={role ? { background: roleColor(role).bg, color: roleColor(role).text, borderColor: `${roleColor(role).border}33` } : undefined}
117
- >
118
- {role ? CHAT_ROLES.find((r) => r.id === role)?.label || role : 'Role'}
119
- {role ? (
120
- <X
121
- size={10}
122
- className="opacity-60 hover:opacity-100"
123
- onClick={(e) => { e.stopPropagation(); onRoleChange?.(null); }}
124
- />
125
- ) : (
126
- <ChevronDown size={10} />
127
- )}
128
- </button>
129
- {roleMenuOpen && (
130
- <div className="absolute left-0 top-full mt-1 w-56 rounded-lg border border-border bg-surface-1 shadow-xl z-50 py-1 max-h-72 overflow-y-auto">
131
- {CHAT_ROLES.map((r) => {
132
- const rc = roleColor(r.id);
133
- const active = role === r.id;
134
- return (
135
- <button
136
- key={r.id}
137
- onClick={() => { onRoleChange?.(r.id); setRoleMenuOpen(false); }}
138
- className={cn(
139
- 'w-full flex items-center gap-2.5 px-3 py-2 text-left hover:bg-surface-3 cursor-pointer transition-colors',
140
- active && 'bg-surface-3',
141
- )}
142
- >
143
- <span
144
- className="w-2 h-2 rounded-full flex-shrink-0"
145
- style={{ background: rc.text }}
146
- />
147
- <div className="flex-1 min-w-0">
148
- <div className="text-xs font-semibold font-sans text-text-0">{r.label}</div>
149
- <div className="text-2xs text-text-3 font-sans">{r.desc}</div>
150
- </div>
151
- {active && <span className="text-2xs text-accent font-mono">active</span>}
152
- </button>
153
- );
154
- })}
155
- </div>
156
- )}
157
- </div>
158
-
159
- {tokens > 0 && (
160
- <span className="text-2xs text-text-3 font-mono tabular-nums">{fmtNum(tokens)} tok</span>
161
- )}
162
-
163
- <div ref={menuRef} className="relative">
164
- <button
165
- onClick={() => setMenuOpen(!menuOpen)}
166
- className="w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer"
167
- >
168
- <MoreHorizontal size={14} />
169
- </button>
170
- {menuOpen && (
171
- <div className="absolute right-0 top-full mt-1 w-40 rounded-md border border-border bg-surface-1 shadow-xl z-50 py-1">
172
- <button
173
- onClick={() => { setEditing(true); setMenuOpen(false); }}
174
- className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-text-1 hover:bg-surface-5 cursor-pointer font-sans"
175
- >
176
- <Pencil size={12} /> Rename
177
- </button>
178
- <button
179
- onClick={() => { pinConversation(conversation.id, !conversation.pinned); setMenuOpen(false); }}
180
- className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-text-1 hover:bg-surface-5 cursor-pointer font-sans"
181
- >
182
- {conversation.pinned ? <PinOff size={12} /> : <Pin size={12} />}
183
- {conversation.pinned ? 'Unpin' : 'Pin'}
184
- </button>
185
- <div className="h-px my-1 bg-border-subtle" />
186
- <button
187
- onClick={() => { deleteConversation(conversation.id); setMenuOpen(false); }}
188
- className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-danger hover:bg-danger/10 cursor-pointer font-sans"
189
- >
190
- <Trash2 size={12} /> Delete
191
- </button>
192
- </div>
193
- )}
194
- </div>
195
- </div>
196
- </div>
197
- );
198
- }
@@ -1,367 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useRef, useEffect, useCallback } from 'react';
3
- import { SendHorizontal, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot, GripHorizontal, ChevronUp } from 'lucide-react';
4
- import { cn } from '../../lib/cn';
5
- import { formatModelName, isImageModel as checkImageModel, getTier, getContextSize, TIER_CONFIG } from './model-picker';
6
- import { useGrooveStore } from '../../stores/groove';
7
- import { Badge } from '../ui/badge';
8
-
9
- const EFFORT_OPTIONS = [
10
- { value: 'none', label: 'None' },
11
- { value: 'low', label: 'Low' },
12
- { value: 'medium', label: 'Med' },
13
- { value: 'high', label: 'High' },
14
- { value: 'xhigh', label: 'XHigh' },
15
- ];
16
-
17
- const VERBOSITY_OPTIONS = [
18
- { value: 'low', label: 'Concise' },
19
- { value: 'medium', label: 'Normal' },
20
- ];
21
-
22
- export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, currentProvider, onModelChange, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange, modeChanging }) {
23
- const [input, setInput] = useState('');
24
- const [inputHeight, setInputHeight] = useState(88);
25
- const [modelPickerOpen, setModelPickerOpen] = useState(false);
26
- const textareaRef = useRef(null);
27
- const fileInputRef = useRef(null);
28
- const modelPickerRef = useRef(null);
29
-
30
- const [providers, setProviders] = useState([]);
31
- const fetchProviders = useGrooveStore((s) => s.fetchProviders);
32
-
33
- useEffect(() => {
34
- fetchProviders().then((data) => {
35
- if (Array.isArray(data)) setProviders(data);
36
- else if (data?.providers) setProviders(data.providers);
37
- }).catch(() => {});
38
- }, [fetchProviders]);
39
-
40
- useEffect(() => {
41
- if (!disabled && textareaRef.current) textareaRef.current.focus();
42
- }, [disabled]);
43
-
44
- useEffect(() => {
45
- if (!modelPickerOpen) return;
46
- function handleClick(e) {
47
- if (modelPickerRef.current && !modelPickerRef.current.contains(e.target)) setModelPickerOpen(false);
48
- }
49
- document.addEventListener('mousedown', handleClick);
50
- return () => document.removeEventListener('mousedown', handleClick);
51
- }, [modelPickerOpen]);
52
-
53
- function handleSend() {
54
- const text = input.trim();
55
- if (!text || sending || disabled) return;
56
- onSend(text);
57
- setInput('');
58
- }
59
-
60
- function onKeyDown(e) {
61
- if (e.key === 'Enter' && !e.shiftKey) {
62
- e.preventDefault();
63
- handleSend();
64
- }
65
- }
66
-
67
- function handleFileSelect(e) {
68
- const files = Array.from(e.target.files || []);
69
- if (files.length === 0) return;
70
- const pathList = files.map((f) => f.name).join(', ');
71
- setInput((prev) => prev + (prev ? '\n' : '') + `[Attached: ${pathList}]`);
72
- e.target.value = '';
73
- }
74
-
75
- const onDragStart = useCallback((e) => {
76
- e.preventDefault();
77
- const startY = e.clientY;
78
- const startH = inputHeight;
79
- const onMove = (ev) => setInputHeight(Math.min(Math.max(56, startH - (ev.clientY - startY)), 400));
80
- const onUp = () => {
81
- window.removeEventListener('mousemove', onMove);
82
- window.removeEventListener('mouseup', onUp);
83
- };
84
- window.addEventListener('mousemove', onMove);
85
- window.addEventListener('mouseup', onUp);
86
- }, [inputHeight]);
87
-
88
- const isActive = streaming || sending;
89
- const canSend = input.trim() && !sending && !disabled;
90
- const currentMode = mode || 'api';
91
-
92
- const placeholder = disabled
93
- ? 'Select a model to start chatting...'
94
- : isImageModel
95
- ? 'Describe the image you want to generate...'
96
- : role
97
- ? `Ask your ${role}...`
98
- : 'Send a message...';
99
-
100
- return (
101
- <div className="px-4 pb-3">
102
- <div
103
- onMouseDown={onDragStart}
104
- className="flex items-center justify-center h-5 cursor-row-resize group"
105
- >
106
- <GripHorizontal size={12} className="text-text-4 group-hover:text-text-2 transition-colors" />
107
- </div>
108
-
109
- {replyContext && (
110
- <div className="flex items-center gap-2 mb-2 px-3 py-2 rounded-lg bg-accent/5 border border-accent/15">
111
- <ImageIcon size={12} className="text-accent flex-shrink-0" />
112
- <span className="flex-1 text-2xs text-text-2 font-sans truncate">Iterating: &quot;{replyContext.prompt}&quot;</span>
113
- <button onClick={onClearReply} className="text-text-4 hover:text-text-1 cursor-pointer flex-shrink-0">
114
- <Square size={10} />
115
- </button>
116
- </div>
117
- )}
118
-
119
- <div className="flex flex-col rounded-lg border border-border-subtle bg-surface-0 transition-colors overflow-hidden focus-within:border-text-4/40">
120
- <input
121
- ref={fileInputRef}
122
- type="file"
123
- multiple
124
- accept=".pdf,.png,.jpg,.jpeg,.gif,.svg,.csv,.txt,.md,.json,.yaml,.yml,.docx,.pptx,.xlsx"
125
- onChange={handleFileSelect}
126
- className="hidden"
127
- />
128
-
129
- <div className="px-1">
130
- <textarea
131
- ref={textareaRef}
132
- value={input}
133
- onChange={(e) => setInput(e.target.value)}
134
- onKeyDown={onKeyDown}
135
- placeholder={placeholder}
136
- disabled={disabled}
137
- rows={1}
138
- className={cn(
139
- 'w-full resize-none px-3 py-2.5 text-[13px]',
140
- 'bg-transparent font-sans text-text-0',
141
- 'placeholder:text-text-4',
142
- 'focus:outline-none',
143
- 'disabled:opacity-40 disabled:cursor-not-allowed',
144
- )}
145
- style={{ height: inputHeight }}
146
- />
147
- </div>
148
-
149
- <div className="flex items-center gap-1.5 px-1.5 pb-1.5 pt-0.5">
150
- <button
151
- onClick={() => fileInputRef.current?.click()}
152
- disabled={disabled}
153
- className="w-7 h-7 flex items-center justify-center rounded-md text-text-4 hover:text-text-1 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
154
- title="Attach file"
155
- >
156
- <Paperclip size={14} />
157
- </button>
158
-
159
- <div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
160
- <button
161
- onClick={() => onModeChange?.('api')}
162
- disabled={modeChanging}
163
- className={cn(
164
- 'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
165
- 'disabled:opacity-50 disabled:cursor-not-allowed',
166
- currentMode === 'api' ? 'bg-accent/15 text-accent' : 'text-text-3 hover:text-text-1',
167
- )}
168
- title="Lightweight — fast and cheap, no tools"
169
- >
170
- <Zap size={10} /> Chat
171
- </button>
172
- <button
173
- onClick={() => onModeChange?.('agent')}
174
- disabled={modeChanging}
175
- className={cn(
176
- 'flex items-center gap-1 h-5 px-2 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
177
- 'disabled:opacity-50 disabled:cursor-not-allowed',
178
- currentMode === 'agent' ? 'bg-purple/15 text-purple' : 'text-text-3 hover:text-text-1',
179
- )}
180
- title="Full agent — tools, files, session resume"
181
- >
182
- <Bot size={10} /> Agent
183
- </button>
184
- </div>
185
-
186
- <div ref={modelPickerRef} className="relative">
187
- <button
188
- onClick={() => setModelPickerOpen(!modelPickerOpen)}
189
- className={cn(
190
- 'flex items-center gap-1 h-5 px-2 rounded text-2xs font-mono transition-colors cursor-pointer',
191
- isImageModel
192
- ? 'bg-purple/8 text-purple hover:bg-purple/15'
193
- : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
194
- )}
195
- >
196
- {isImageModel && <ImageIcon size={9} />}
197
- <span className="max-w-[120px] truncate">{currentModel ? formatModelName(currentModel) : 'Select model'}</span>
198
- <ChevronUp size={10} className="text-text-4 flex-shrink-0" />
199
- </button>
200
-
201
- {modelPickerOpen && (() => {
202
- const chatProviders = [];
203
- const imageProviders = [];
204
- for (const provider of providers) {
205
- const models = provider.models || [];
206
- const chat = [];
207
- const img = [];
208
- for (const m of models) {
209
- const mid = typeof m === 'string' ? m : m.id || m.name;
210
- const mtype = typeof m === 'object' ? m.type : undefined;
211
- if (mtype === 'image' || checkImageModel(mid)) img.push(m);
212
- else chat.push(m);
213
- }
214
- if (chat.length) chatProviders.push({ ...provider, models: chat });
215
- if (img.length) imageProviders.push({ ...provider, models: img });
216
- }
217
-
218
- const renderModel = (provider, model) => {
219
- const modelId = typeof model === 'string' ? model : model.id || model.name;
220
- const modelName = typeof model === 'string' ? model : model.name || model.id;
221
- const mtype = typeof model === 'object' ? model.type : undefined;
222
- const isImg = mtype === 'image' || checkImageModel(modelId);
223
- const tier = isImg ? null : getTier(modelId);
224
- const tierCfg = tier ? TIER_CONFIG[tier] : null;
225
- const TierIcon = tierCfg?.icon;
226
- const isActive = currentModel === modelId && currentProvider === provider.id;
227
- return (
228
- <button
229
- key={modelId}
230
- onClick={() => {
231
- onModelChange?.({ provider: provider.id, model: modelId });
232
- setModelPickerOpen(false);
233
- }}
234
- className={cn(
235
- 'w-full flex items-center gap-3 px-3.5 py-2.5 text-left transition-colors cursor-pointer',
236
- isActive ? 'bg-accent/10' : 'hover:bg-surface-3',
237
- )}
238
- >
239
- <div className="flex-1 min-w-0">
240
- <div className="flex items-center gap-1.5">
241
- {isImg && <ImageIcon size={11} className="text-purple flex-shrink-0" />}
242
- <span className={cn('text-xs font-medium font-sans truncate', isActive ? 'text-accent' : 'text-text-0')}>{modelName}</span>
243
- </div>
244
- {!isImg && (
245
- <div className="text-2xs text-text-4 font-sans mt-0.5">{getContextSize(modelId)} context</div>
246
- )}
247
- </div>
248
- <div className="flex items-center gap-1.5 flex-shrink-0">
249
- {isImg ? (
250
- <Badge variant="purple" className="text-[9px]">
251
- <ImageIcon size={8} /> Image
252
- </Badge>
253
- ) : tierCfg && (
254
- <Badge variant={tierCfg.variant} className="text-[9px]">
255
- <TierIcon size={8} /> {tierCfg.label}
256
- </Badge>
257
- )}
258
- </div>
259
- </button>
260
- );
261
- };
262
-
263
- return (
264
- <div className="absolute bottom-full left-0 mb-1.5 w-80 max-h-[60vh] overflow-y-auto rounded-xl border border-border bg-surface-1 shadow-2xl z-50">
265
- {providers.length === 0 && (
266
- <div className="px-4 py-8 text-center text-xs text-text-3 font-sans">No providers available</div>
267
- )}
268
- {chatProviders.map((provider) => (
269
- <div key={provider.id}>
270
- <div className="px-3.5 py-2 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2/80 border-b border-border-subtle sticky top-0 backdrop-blur-sm">
271
- {provider.name || provider.id}
272
- </div>
273
- {provider.models.map((m) => renderModel(provider, m))}
274
- </div>
275
- ))}
276
- {imageProviders.length > 0 && (
277
- <>
278
- <div className="px-3.5 py-2 text-2xs font-semibold text-text-4 uppercase tracking-wider font-sans bg-surface-0 border-y border-border-subtle flex items-center gap-1.5 sticky top-0 backdrop-blur-sm">
279
- <ImageIcon size={10} className="text-purple" />
280
- Image Generation
281
- </div>
282
- {imageProviders.map((provider) => (
283
- <div key={`img-${provider.id}`}>
284
- <div className="px-3.5 py-2 text-2xs font-semibold text-text-3 uppercase tracking-wider font-sans bg-surface-2/80 border-b border-border-subtle">
285
- {provider.name || provider.id}
286
- </div>
287
- {provider.models.map((m) => renderModel(provider, m))}
288
- </div>
289
- ))}
290
- </>
291
- )}
292
- </div>
293
- );
294
- })()}
295
- </div>
296
-
297
- {isCodex && (
298
- <>
299
- <div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
300
- {EFFORT_OPTIONS.map((opt) => (
301
- <button
302
- key={opt.value}
303
- onClick={() => onReasoningEffortChange?.(opt.value)}
304
- className={cn(
305
- 'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
306
- reasoningEffort === opt.value
307
- ? 'bg-accent/15 text-accent'
308
- : 'text-text-4 hover:text-text-1',
309
- )}
310
- title={`Reasoning: ${opt.label}`}
311
- >
312
- {opt.label}
313
- </button>
314
- ))}
315
- </div>
316
-
317
- <div className="flex items-center h-5 rounded bg-surface-3 border border-border-subtle p-0.5">
318
- {VERBOSITY_OPTIONS.map((opt) => (
319
- <button
320
- key={opt.value}
321
- onClick={() => onVerbosityChange?.(opt.value)}
322
- className={cn(
323
- 'h-4 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
324
- verbosity === opt.value
325
- ? 'bg-accent/15 text-accent'
326
- : 'text-text-4 hover:text-text-1',
327
- )}
328
- title={`Verbosity: ${opt.label}`}
329
- >
330
- {opt.label}
331
- </button>
332
- ))}
333
- </div>
334
- </>
335
- )}
336
-
337
- <div className="flex-1" />
338
-
339
- {isActive ? (
340
- <button
341
- onClick={onStop}
342
- title="Stop generation"
343
- className="group w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer"
344
- >
345
- <span className="relative flex items-center justify-center w-3.5 h-3.5">
346
- <span className="absolute inset-0 rounded-full bg-accent/30 group-hover:bg-red-500/30 animate-ping [animation-duration:2s] transition-colors" />
347
- <span className="relative w-2.5 h-2.5 rounded-full bg-accent group-hover:bg-red-500 transition-colors" />
348
- </span>
349
- </button>
350
- ) : (
351
- <button
352
- onClick={handleSend}
353
- disabled={!canSend}
354
- className={cn(
355
- 'w-7 h-7 flex items-center justify-center rounded-md transition-colors cursor-pointer',
356
- 'disabled:opacity-15 disabled:cursor-not-allowed',
357
- canSend ? 'text-text-0 hover:text-text-1' : 'text-text-4',
358
- )}
359
- >
360
- {sending ? <Loader2 size={15} className="animate-spin" /> : <SendHorizontal size={15} />}
361
- </button>
362
- )}
363
- </div>
364
- </div>
365
- </div>
366
- );
367
- }