lvwerra HF Staff commited on
Commit
088085b
·
verified ·
1 Parent(s): ba0ce47

New-agent logo-button picker; new-group per-agent quantity cart

Browse files
web/src/App.tsx CHANGED
@@ -83,8 +83,14 @@ export default function App() {
83
  await refresh();
84
  setActiveRef(`s:${s.id}`);
85
  };
86
- const newGroup = async (name: string) => {
87
  const g = await api.createGroup(name);
 
 
 
 
 
 
88
  await refresh();
89
  setActiveRef(`g:${g.id}`);
90
  };
 
83
  await refresh();
84
  setActiveRef(`s:${s.id}`);
85
  };
86
+ const newGroup = async (name: string, cart?: { cli: string; count: number }[]) => {
87
  const g = await api.createGroup(name);
88
+ for (const { cli, count } of cart || []) {
89
+ const base = cliMap[cli]?.label || cli;
90
+ for (let i = 0; i < count; i++) {
91
+ await api.createSession(count > 1 ? `${base} ${i + 1}` : base, cli, g.id);
92
+ }
93
+ }
94
  await refresh();
95
  setActiveRef(`g:${g.id}`);
96
  };
web/src/components/NewSession.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import { useState } from 'react';
2
  import type { Cli } from '../types';
 
3
 
4
  export default function NewSession({
5
  clis, onCreate, onCancel,
@@ -9,30 +10,26 @@ export default function NewSession({
9
  onCancel?: () => void;
10
  }) {
11
  const [name, setName] = useState('');
12
- const firstAvail = clis.find((c) => c.available)?.id || 'shell';
13
- const [cli, setCli] = useState(firstAvail);
14
- const submit = () => { onCreate(name.trim(), cli); setName(''); };
15
 
16
  return (
17
  <div className="widget">
18
  <input
19
  autoFocus
20
- placeholder="Agent name (e.g. refactor-api)"
21
  value={name}
22
  onChange={(e) => setName(e.target.value)}
23
- onKeyDown={(e) => { if (e.key === 'Enter') submit(); if (e.key === 'Escape') onCancel?.(); }}
24
  />
25
- <select value={cli} onChange={(e) => setCli(e.target.value)}>
26
- {clis.map((c) => (
27
- <option key={c.id} value={c.id} disabled={!c.available}>
28
- {c.label}{c.available ? '' : ' — unavailable'}
29
- </option>
 
30
  ))}
31
- </select>
32
- <div className="widget-actions">
33
- <button className="btn-primary" onClick={submit}>Create agent</button>
34
- {onCancel && <button className="btn-ghost" onClick={onCancel}>Cancel</button>}
35
  </div>
 
36
  </div>
37
  );
38
  }
 
1
  import { useState } from 'react';
2
  import type { Cli } from '../types';
3
+ import Logo from './Logo';
4
 
5
  export default function NewSession({
6
  clis, onCreate, onCancel,
 
10
  onCancel?: () => void;
11
  }) {
12
  const [name, setName] = useState('');
13
+ const pick = (c: Cli) => { onCreate(name.trim() || c.label, c.id); setName(''); };
 
 
14
 
15
  return (
16
  <div className="widget">
17
  <input
18
  autoFocus
19
+ placeholder="Agent name (optional)"
20
  value={name}
21
  onChange={(e) => setName(e.target.value)}
22
+ onKeyDown={(e) => { if (e.key === 'Escape') onCancel?.(); }}
23
  />
24
+ <div className="agent-picks">
25
+ {clis.filter((c) => c.available).map((c) => (
26
+ <button key={c.id} className="agent-pick" title={`New ${c.label}`} onClick={() => pick(c)}>
27
+ <Logo cli={c.id} size={16} />
28
+ <span>{c.label}</span>
29
+ </button>
30
  ))}
 
 
 
 
31
  </div>
32
+ {onCancel && <button className="btn-ghost" onClick={onCancel}>Cancel</button>}
33
  </div>
34
  );
35
  }
web/src/components/Sidebar.tsx CHANGED
@@ -19,7 +19,7 @@ export default function Sidebar({
19
  onOpenSession: (sessionId: string, groupId?: string) => void;
20
  onOpenSettings: () => void;
21
  onNewSession: (name: string, cli: string) => void;
22
- onNewGroup: (name: string) => void;
23
  onRenameGroup: (id: string, name: string) => void;
24
  onRenameSession: (id: string, name: string) => void;
25
  onDeleteGroup: (id: string) => void;
@@ -31,6 +31,7 @@ export default function Sidebar({
31
  }) {
32
  const [panel, setPanel] = useState<'none' | 'session' | 'group'>('none');
33
  const [groupName, setGroupName] = useState('');
 
34
  const [editRef, setEditRef] = useState<string | null>(null);
35
  const [editName, setEditName] = useState('');
36
  const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
@@ -41,7 +42,12 @@ export default function Sidebar({
41
  const groupById = useMemo(() => Object.fromEntries(tree.groups.map((g) => [g.id, g])), [tree.groups]);
42
 
43
  const clearDrag = () => { setDragRef(null); setDrop(null); };
44
- const submitGroup = () => { onNewGroup(groupName.trim()); setGroupName(''); setPanel('none'); };
 
 
 
 
 
45
  const startEdit = (ref: string, name: string) => { setEditRef(ref); setEditName(name); };
46
  const commitEdit = () => {
47
  if (editRef) {
@@ -186,9 +192,25 @@ export default function Sidebar({
186
  <input autoFocus placeholder="Group name" value={groupName}
187
  onChange={(e) => setGroupName(e.target.value)}
188
  onKeyDown={(e) => { if (e.key === 'Enter') submitGroup(); if (e.key === 'Escape') setPanel('none'); }} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  <div className="widget-actions">
190
  <button className="btn-primary" onClick={submitGroup}>Create group</button>
191
- <button className="btn-ghost" onClick={() => setPanel('none')}>Cancel</button>
192
  </div>
193
  </div>
194
  )}
 
19
  onOpenSession: (sessionId: string, groupId?: string) => void;
20
  onOpenSettings: () => void;
21
  onNewSession: (name: string, cli: string) => void;
22
+ onNewGroup: (name: string, cart?: { cli: string; count: number }[]) => void;
23
  onRenameGroup: (id: string, name: string) => void;
24
  onRenameSession: (id: string, name: string) => void;
25
  onDeleteGroup: (id: string) => void;
 
31
  }) {
32
  const [panel, setPanel] = useState<'none' | 'session' | 'group'>('none');
33
  const [groupName, setGroupName] = useState('');
34
+ const [cart, setCart] = useState<Record<string, number>>({});
35
  const [editRef, setEditRef] = useState<string | null>(null);
36
  const [editName, setEditName] = useState('');
37
  const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
 
42
  const groupById = useMemo(() => Object.fromEntries(tree.groups.map((g) => [g.id, g])), [tree.groups]);
43
 
44
  const clearDrag = () => { setDragRef(null); setDrop(null); };
45
+ const bump = (id: string, d: number) => setCart((c) => ({ ...c, [id]: Math.max(0, (c[id] || 0) + d) }));
46
+ const submitGroup = () => {
47
+ const items = Object.entries(cart).filter(([, n]) => n > 0).map(([cli, count]) => ({ cli, count }));
48
+ onNewGroup(groupName.trim() || 'Group', items);
49
+ setGroupName(''); setCart({}); setPanel('none');
50
+ };
51
  const startEdit = (ref: string, name: string) => { setEditRef(ref); setEditName(name); };
52
  const commitEdit = () => {
53
  if (editRef) {
 
192
  <input autoFocus placeholder="Group name" value={groupName}
193
  onChange={(e) => setGroupName(e.target.value)}
194
  onKeyDown={(e) => { if (e.key === 'Enter') submitGroup(); if (e.key === 'Escape') setPanel('none'); }} />
195
+ <div className="cart">
196
+ {clis.filter((c) => c.available).map((c) => {
197
+ const n = cart[c.id] || 0;
198
+ return (
199
+ <div key={c.id} className={`cart-row${n > 0 ? ' has' : ''}`}>
200
+ <div className="stepper">
201
+ <button onClick={() => bump(c.id, -1)} disabled={n === 0} aria-label="Fewer">−</button>
202
+ <span className="stepper-n">{n}</span>
203
+ <button onClick={() => bump(c.id, 1)} aria-label="More">+</button>
204
+ </div>
205
+ <Logo cli={c.id} size={16} />
206
+ <span className="cart-name">{c.label}</span>
207
+ </div>
208
+ );
209
+ })}
210
+ </div>
211
  <div className="widget-actions">
212
  <button className="btn-primary" onClick={submitGroup}>Create group</button>
213
+ <button className="btn-ghost" onClick={() => { setCart({}); setPanel('none'); }}>Cancel</button>
214
  </div>
215
  </div>
216
  )}
web/src/styles.css CHANGED
@@ -93,6 +93,22 @@ body {
93
  .widget-actions { display: flex; gap: 8px; }
94
  .widget-actions .btn-primary { flex: 1; }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  /* tree */
97
  .tree { flex: 1; overflow-y: auto; padding: 6px 8px 10px; }
98
  .empty-hint { color: var(--muted); font-size: 12px; padding: 12px 10px; line-height: 1.5; }
 
93
  .widget-actions { display: flex; gap: 8px; }
94
  .widget-actions .btn-primary { flex: 1; }
95
 
96
+ /* new-agent: logo buttons */
97
+ .agent-picks { display: flex; flex-direction: column; gap: 4px; }
98
+ .agent-pick { display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; color: var(--text); font: inherit; font-size: 13px; cursor: pointer; text-align: left; }
99
+ .agent-pick:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--panel)); }
100
+
101
+ /* new-group: per-agent quantity cart */
102
+ .cart { display: flex; flex-direction: column; gap: 4px; }
103
+ .cart-row { display: flex; align-items: center; gap: 8px; padding: 3px 4px; border-radius: 8px; font-size: 13px; }
104
+ .cart-row.has { background: color-mix(in srgb, var(--accent) 8%, transparent); }
105
+ .cart-name { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
106
+ .stepper { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; flex: none; background: var(--panel); }
107
+ .stepper button { width: 24px; height: 24px; border: none; background: transparent; color: var(--muted); font-size: 15px; line-height: 1; cursor: pointer; }
108
+ .stepper button:hover:not(:disabled) { color: var(--text); background: var(--panel-2); }
109
+ .stepper button:disabled { opacity: 0.35; cursor: not-allowed; }
110
+ .stepper-n { min-width: 20px; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
111
+
112
  /* tree */
113
  .tree { flex: 1; overflow-y: auto; padding: 6px 8px 10px; }
114
  .empty-hint { color: var(--muted); font-size: 12px; padding: 12px 10px; line-height: 1.5; }