Spaces:
Running
Running
New-agent logo-button picker; new-group per-agent quantity cart
Browse files- web/src/App.tsx +7 -1
- web/src/components/NewSession.tsx +11 -14
- web/src/components/Sidebar.tsx +25 -3
- web/src/styles.css +16 -0
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
|
| 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 (
|
| 21 |
value={name}
|
| 22 |
onChange={(e) => setName(e.target.value)}
|
| 23 |
-
onKeyDown={(e) => { if (e.key === '
|
| 24 |
/>
|
| 25 |
-
<
|
| 26 |
-
{clis.map((c) => (
|
| 27 |
-
<
|
| 28 |
-
{c.
|
| 29 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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; }
|